summaryrefslogtreecommitdiffstats
path: root/testing/marionette/harness
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/marionette/harness/MANIFEST.in4
-rw-r--r--testing/marionette/harness/README.rst30
-rw-r--r--testing/marionette/harness/marionette_harness/__init__.py32
-rw-r--r--testing/marionette/harness/marionette_harness/certificates/test.cert86
-rw-r--r--testing/marionette/harness/marionette_harness/certificates/test.key28
-rw-r--r--testing/marionette/harness/marionette_harness/marionette_test/__init__.py24
-rw-r--r--testing/marionette/harness/marionette_harness/marionette_test/decorators.py194
-rw-r--r--testing/marionette/harness/marionette_harness/marionette_test/testcases.py420
-rw-r--r--testing/marionette/harness/marionette_harness/runner/__init__.py16
-rw-r--r--testing/marionette/harness/marionette_harness/runner/base.py1265
-rwxr-xr-xtesting/marionette/harness/marionette_harness/runner/httpd.py243
-rw-r--r--testing/marionette/harness/marionette_harness/runner/mixins/__init__.py5
-rw-r--r--testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py210
-rwxr-xr-xtesting/marionette/harness/marionette_harness/runner/serve.py239
-rw-r--r--testing/marionette/harness/marionette_harness/runtests.py115
-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
-rw-r--r--testing/marionette/harness/marionette_harness/www/actions_scroll.html139
-rw-r--r--testing/marionette/harness/marionette_harness/www/addons/webextension-signed.xpibin0 -> 4221 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/addons/webextension-unsigned.xpibin0 -> 310 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/black.pngbin0 -> 150 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/bug814037.html56
-rw-r--r--testing/marionette/harness/marionette_harness/www/click_out_of_bounds_overflow.html90
-rw-r--r--testing/marionette/harness/marionette_harness/www/clicks.html57
-rw-r--r--testing/marionette/harness/marionette_harness/www/dom/cache/basicCacheAPI_PBM.html21
-rw-r--r--testing/marionette/harness/marionette_harness/www/dom/cache/cacheUsage.html28
-rw-r--r--testing/marionette/harness/marionette_harness/www/dom/indexedDB/basicIDB_PBM.html49
-rw-r--r--testing/marionette/harness/marionette_harness/www/element_outside_viewport.html41
-rw-r--r--testing/marionette/harness/marionette_harness/www/empty.html12
-rw-r--r--testing/marionette/harness/marionette_harness/www/formPage.html114
-rw-r--r--testing/marionette/harness/marionette_harness/www/frameset.html13
-rw-r--r--testing/marionette/harness/marionette_harness/www/framesetPage2.html7
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/blue.jpgbin0 -> 92 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/boolean_attributes.html2
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/geolocation.js29
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/green.jpgbin0 -> 92 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/offline.html1
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/red.jpgbin0 -> 92 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/status.html1
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/test.appcache11
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/test_html_inputs.html2
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5/yellow.jpgbin0 -> 92 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/html5Page.html46
-rw-r--r--testing/marionette/harness/marionette_harness/www/keyboard.html99
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_columns.html31
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_cursor.html31
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_display_none.html10
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe.html15
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll.html11
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll_inner.html24
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_key_scroll.html18
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_longtext.html9
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_multipleline.html18
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_multiplerange.html19
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_selection.html49
-rw-r--r--testing/marionette/harness/marionette_harness/www/layout/test_carets_svg_shapes.html12
-rw-r--r--testing/marionette/harness/marionette_harness/www/navigation_pushstate.html20
-rw-r--r--testing/marionette/harness/marionette_harness/www/navigation_pushstate_target.html13
-rw-r--r--testing/marionette/harness/marionette_harness/www/nestedElements.html9
-rw-r--r--testing/marionette/harness/marionette_harness/www/reftest/mostly-teal-700x700.html21
-rw-r--r--testing/marionette/harness/marionette_harness/www/reftest/teal-700x700.html21
-rw-r--r--testing/marionette/harness/marionette_harness/www/resultPage.html16
-rw-r--r--testing/marionette/harness/marionette_harness/www/serviceworker/install_serviceworker.html11
-rw-r--r--testing/marionette/harness/marionette_harness/www/serviceworker/serviceworker.js0
-rw-r--r--testing/marionette/harness/marionette_harness/www/shim.js297
-rw-r--r--testing/marionette/harness/marionette_harness/www/slow_resource.html13
-rw-r--r--testing/marionette/harness/marionette_harness/www/test.html43
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_accessibility.html57
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_clearing.html24
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_dynamic.html38
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_iframe.html16
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_inner_iframe.html13
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_nested_iframe.html13
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_oop_1.html14
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_oop_2.html14
-rw-r--r--testing/marionette/harness/marionette_harness/www/test_windows.html13
-rw-r--r--testing/marionette/harness/marionette_harness/www/update/complete.marbin0 -> 86612 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/update/complete.mar.headers1
-rw-r--r--testing/marionette/harness/marionette_harness/www/visibility.html51
-rw-r--r--testing/marionette/harness/marionette_harness/www/white.pngbin0 -> 150 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/www/windowHandles.html16
-rw-r--r--testing/marionette/harness/marionette_harness/www/xhtmlTest.html79
-rw-r--r--testing/marionette/harness/requirements.txt15
-rw-r--r--testing/marionette/harness/setup.py58
187 files changed, 18432 insertions, 0 deletions
diff --git a/testing/marionette/harness/MANIFEST.in b/testing/marionette/harness/MANIFEST.in
new file mode 100644
index 0000000000..ce2d97cd30
--- /dev/null
+++ b/testing/marionette/harness/MANIFEST.in
@@ -0,0 +1,4 @@
+exclude MANIFEST.in
+include requirements.txt
+recursive-include marionette_harness/certificates *
+recursive-include marionette_harness/www *
diff --git a/testing/marionette/harness/README.rst b/testing/marionette/harness/README.rst
new file mode 100644
index 0000000000..3f8865603e
--- /dev/null
+++ b/testing/marionette/harness/README.rst
@@ -0,0 +1,30 @@
+marionette-harness
+==================
+
+Marionette is an automation driver for Mozilla's Gecko engine. It can remotely
+control either the UI or the internal JavaScript of a Gecko platform, such as
+Firefox. It can control both the chrome (i.e. menus and functions) or the
+content (the webpage loaded inside the browsing context), giving a high level
+of control and ability to replicate user actions. In addition to performing
+actions on the browser, Marionette can also read the properties and attributes
+of the DOM.
+
+The marionette_harness package contains the test runner for Marionette, and
+allows you to run automated tests written in Python for Gecko based
+applications. Therefore it offers the necessary testcase classes, which are
+based on the unittest framework.
+
+For more information and the repository please checkout:
+
+- home and docs: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette
+
+
+Example
+-------
+
+The following command will run the tests as specified via a manifest file, or
+test path, or test folder in Firefox:
+
+ marionette --binary %path_to_firefox% [manifest_file | test_file | test_folder]
+
+To get an overview about all possible option run `marionette --help`.
diff --git a/testing/marionette/harness/marionette_harness/__init__.py b/testing/marionette/harness/marionette_harness/__init__.py
new file mode 100644
index 0000000000..25e18ef56f
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/__init__.py
@@ -0,0 +1,32 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+__version__ = "5.0.2"
+
+from .marionette_test import (
+ CommonTestCase,
+ MarionetteTestCase,
+ SkipTest,
+ expectedFailure,
+ parameterized,
+ run_if_manage_instance,
+ skip,
+ skip_if_chrome,
+ skip_if_desktop,
+ skip_unless_browser_pref,
+ skip_unless_protocol,
+ unexpectedSuccess,
+)
+from .runner import (
+ BaseMarionetteArguments,
+ BaseMarionetteTestRunner,
+ Marionette,
+ MarionetteTest,
+ MarionetteTestResult,
+ MarionetteTextTestRunner,
+ TestManifest,
+ TestResult,
+ TestResultCollection,
+ WindowManagerMixin,
+)
diff --git a/testing/marionette/harness/marionette_harness/certificates/test.cert b/testing/marionette/harness/marionette_harness/certificates/test.cert
new file mode 100644
index 0000000000..3fd1cba2b7
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/certificates/test.cert
@@ -0,0 +1,86 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 2 (0x2)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: CN=web-platform-tests
+ Validity
+ Not Before: Dec 22 12:09:16 2014 GMT
+ Not After : Dec 21 12:09:16 2024 GMT
+ Subject: CN=web-platform.test
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:b3:84:d6:8b:01:59:18:85:d1:dc:32:df:38:f7:
+ 90:85:1b:3e:a5:5e:81:3e:2f:fc:3a:5f:7f:77:ef:
+ 23:bb:3a:88:27:0f:be:25:46:cd:63:7d:cb:95:d8:
+ a5:50:10:d2:a2:d2:b7:97:d1:0d:6c:fb:f9:05:e8:
+ 6f:a8:4b:bd:95:67:9e:7b:94:58:a9:6d:93:fd:e0:
+ 12:c5:cd:b4:8a:64:52:31:5f:0e:e3:89:84:71:da:
+ 98:dd:4b:ec:02:25:a5:7d:35:fe:63:da:b3:ac:ec:
+ a5:46:0f:0d:64:23:5c:6d:f3:ec:cc:28:63:23:c0:
+ 4b:9a:ec:8f:c1:ee:b1:a2:3e:72:4d:70:b5:09:c1:
+ eb:b4:10:55:3c:8b:ea:1b:94:7e:4b:74:e6:f4:9f:
+ 4f:a6:45:30:b5:f0:b8:b4:d1:59:50:65:0a:86:53:
+ ea:4c:9f:9e:f4:58:6c:31:f5:17:3a:6f:57:8b:cb:
+ 5f:f0:28:0b:45:92:8d:30:20:49:ff:52:e6:2c:cb:
+ 18:9a:d7:e6:ee:3e:4f:34:35:15:13:c5:02:da:c5:
+ 5f:be:fb:5b:ce:8d:bf:b5:35:76:3c:7c:e6:9c:3b:
+ 26:87:4d:8d:80:e6:16:c6:27:f2:50:49:b6:72:74:
+ 43:49:49:44:38:bb:78:43:23:ee:16:3e:d9:62:e6:
+ a5:d7
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ X509v3 Subject Key Identifier:
+ 2D:98:A3:99:39:1C:FE:E9:9A:6D:17:94:D2:3A:96:EE:C8:9E:04:22
+ X509v3 Authority Key Identifier:
+ keyid:6A:AB:53:64:92:36:87:23:34:B3:1D:6F:85:4B:F5:DF:5A:5C:74:8F
+
+ X509v3 Key Usage:
+ Digital Signature, Non Repudiation, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication
+ X509v3 Subject Alternative Name:
+ DNS:web-platform.test, DNS:www.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--lve-6lad.web-platform.test, DNS:www2.web-platform.test, DNS:www1.web-platform.test
+ Signature Algorithm: sha256WithRSAEncryption
+ 33:db:f7:f0:f6:92:16:4f:2d:42:bc:b8:aa:e6:ab:5e:f9:b9:
+ b0:48:ae:b5:8d:cc:02:7b:e9:6f:4e:75:f7:17:a0:5e:7b:87:
+ 06:49:48:83:c5:bb:ca:95:07:37:0e:5d:e3:97:de:9e:0c:a4:
+ 82:30:11:81:49:5d:50:29:72:92:a5:ca:17:b1:7c:f1:32:11:
+ 17:57:e6:59:c1:ac:e3:3b:26:d2:94:97:50:6a:b9:54:88:84:
+ 9b:6f:b1:06:f5:80:04:22:10:14:b1:f5:97:25:fc:66:d6:69:
+ a3:36:08:85:23:ff:8e:3c:2b:e0:6d:e7:61:f1:00:8f:61:3d:
+ b0:87:ad:72:21:f6:f0:cc:4f:c9:20:bf:83:11:0f:21:f4:b8:
+ c0:dd:9c:51:d7:bb:27:32:ec:ab:a4:62:14:28:32:da:f2:87:
+ 80:68:9c:ea:ac:eb:f5:7f:f5:de:f4:c0:39:91:c8:76:a4:ee:
+ d0:a8:50:db:c1:4b:f9:c4:3d:d9:e8:8e:b6:3f:c0:96:79:12:
+ d8:fa:4d:0a:b3:36:76:aa:4e:b2:82:2f:a2:d4:0d:db:fd:64:
+ 77:6f:6e:e9:94:7f:0f:c8:3a:3c:96:3d:cd:4d:6c:ba:66:95:
+ f7:b4:9d:a4:94:9f:97:b3:9a:0d:dc:18:8c:11:0b:56:65:8e:
+ 46:4c:e6:5e
+-----BEGIN CERTIFICATE-----
+MIID2jCCAsKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJ3ZWIt
+cGxhdGZvcm0tdGVzdHMwHhcNMTQxMjIyMTIwOTE2WhcNMjQxMjIxMTIwOTE2WjAc
+MRowGAYDVQQDExF3ZWItcGxhdGZvcm0udGVzdDCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBALOE1osBWRiF0dwy3zj3kIUbPqVegT4v/Dpff3fvI7s6iCcP
+viVGzWN9y5XYpVAQ0qLSt5fRDWz7+QXob6hLvZVnnnuUWKltk/3gEsXNtIpkUjFf
+DuOJhHHamN1L7AIlpX01/mPas6zspUYPDWQjXG3z7MwoYyPAS5rsj8HusaI+ck1w
+tQnB67QQVTyL6huUfkt05vSfT6ZFMLXwuLTRWVBlCoZT6kyfnvRYbDH1FzpvV4vL
+X/AoC0WSjTAgSf9S5izLGJrX5u4+TzQ1FRPFAtrFX777W86Nv7U1djx85pw7JodN
+jYDmFsYn8lBJtnJ0Q0lJRDi7eEMj7hY+2WLmpdcCAwEAAaOCASQwggEgMAkGA1Ud
+EwQCMAAwHQYDVR0OBBYEFC2Yo5k5HP7pmm0XlNI6lu7IngQiMB8GA1UdIwQYMBaA
+FGqrU2SSNocjNLMdb4VL9d9aXHSPMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAKBggr
+BgEFBQcDATCBsAYDVR0RBIGoMIGlghF3ZWItcGxhdGZvcm0udGVzdIIVd3d3Lndl
+Yi1wbGF0Zm9ybS50ZXN0gil4bi0tbjhqNmRzNTNsd3drcnFodjI4YS53ZWItcGxh
+dGZvcm0udGVzdIIeeG4tLWx2ZS02bGFkLndlYi1wbGF0Zm9ybS50ZXN0ghZ3d3cy
+LndlYi1wbGF0Zm9ybS50ZXN0ghZ3d3cxLndlYi1wbGF0Zm9ybS50ZXN0MA0GCSqG
+SIb3DQEBCwUAA4IBAQAz2/fw9pIWTy1CvLiq5qte+bmwSK61jcwCe+lvTnX3F6Be
+e4cGSUiDxbvKlQc3Dl3jl96eDKSCMBGBSV1QKXKSpcoXsXzxMhEXV+ZZwazjOybS
+lJdQarlUiISbb7EG9YAEIhAUsfWXJfxm1mmjNgiFI/+OPCvgbedh8QCPYT2wh61y
+IfbwzE/JIL+DEQ8h9LjA3ZxR17snMuyrpGIUKDLa8oeAaJzqrOv1f/Xe9MA5kch2
+pO7QqFDbwUv5xD3Z6I62P8CWeRLY+k0KszZ2qk6ygi+i1A3b/WR3b27plH8PyDo8
+lj3NTWy6ZpX3tJ2klJ+Xs5oN3BiMEQtWZY5GTOZe
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/testing/marionette/harness/marionette_harness/certificates/test.key b/testing/marionette/harness/marionette_harness/certificates/test.key
new file mode 100644
index 0000000000..194a49ec42
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/certificates/test.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzhNaLAVkYhdHc
+Mt8495CFGz6lXoE+L/w6X3937yO7OognD74lRs1jfcuV2KVQENKi0reX0Q1s+/kF
+6G+oS72VZ557lFipbZP94BLFzbSKZFIxXw7jiYRx2pjdS+wCJaV9Nf5j2rOs7KVG
+Dw1kI1xt8+zMKGMjwEua7I/B7rGiPnJNcLUJweu0EFU8i+oblH5LdOb0n0+mRTC1
+8Li00VlQZQqGU+pMn570WGwx9Rc6b1eLy1/wKAtFko0wIEn/UuYsyxia1+buPk80
+NRUTxQLaxV+++1vOjb+1NXY8fOacOyaHTY2A5hbGJ/JQSbZydENJSUQ4u3hDI+4W
+Ptli5qXXAgMBAAECggEBAIcwDQSnIjo2ZECHytQykpG6X6XXEksLhc1Lp0lhPC49
+uNR5pX6a4AcBb3PLr0opMQZO2tUoKA0ff3t0e8loKD+/xXhY0Z/dlioEOP7elwv0
+2nS1mhe9spCuxpk4GGXRhdtR8t2tj8s0do3YvgPgITXoEDX6YBZHNGhZpzSrFPgQ
+/c3eGCVmzWYuLFfdj5OPQ9bwTaY4JSvDLZT0/WTgiica7VySwfz3HP1fFqNykTiK
+ACQREvtxfk5Ym2nT6oni7CM2zOEJL9SXicXI5HO4bERH0ZYh//F3g6mwGiFXUJPd
+NKgaTM1oT9kRGkUaEYsRWrddwR8d5mXLvBuTJbgIsSECgYEA1+2uJSYRW1OqbhYP
+ms59YQHSs3VjpJpnCV2zNa2Wixs57KS2cOH7B6KrQCogJFLtgCDVLtyoErfVkD7E
+FivTgYr1pVCRppJddQzXik31uOINOBVffr7/09g3GcRN+ubHPZPq3K+dD6gHa3Aj
+0nH1EjEEV0QpSTQFn87OF2mc9wcCgYEA1NVqMbbzd+9Xft5FXuSbX6E+S02dOGat
+SgpnkTM80rjqa6eHdQzqk3JqyteHPgdi1vdYRlSPOj/X+6tySY0Ej9sRnYOfddA2
+kpiDiVkmiqVolyJPY69Utj+E3TzJ1vhCQuYknJmB7zP9tDcTxMeq0l/NaWvGshEK
+yC4UTQog1rECgYASOFILfGzWgfbNlzr12xqlRtwanHst9oFfPvLSQrWDQ2bd2wAy
+Aj+GY2mD3oobxouX1i1m6OOdwLlalJFDNauBMNKNgoDnx03vhIfjebSURy7KXrNS
+JJe9rm7n07KoyzRgs8yLlp3wJkOKA0pihY8iW9R78JpzPNqEo5SsURMXnQKBgBlV
+gfuC9H4tPjP6zzUZbyk1701VYsaI6k2q6WMOP0ox+q1v1p7nN7DvaKjWeOG4TVqb
+PKW6gQYE/XeWk9cPcyCQigs+1KdYbnaKsvWRaBYO1GFREzQhdarv6qfPCZOOH40J
+Cgid+Sp4/NULzU2aGspJ3xCSZKdjge4MFhyJfRkxAoGBAJlwqY4nue0MBLGNpqcs
+WwDtSasHvegKAcxGBKL5oWPbLBk7hk+hdqc8f6YqCkCNqv/ooBspL15ESItL+6yT
+zt0YkK4oH9tmLDb+rvqZ7ZdXbWSwKITMoCyyHUtT6OKt/RtA0Vdy9LPnP27oSO/C
+dk8Qf7KgKZLWo0ZNkvw38tEC
+-----END PRIVATE KEY----- \ No newline at end of file
diff --git a/testing/marionette/harness/marionette_harness/marionette_test/__init__.py b/testing/marionette/harness/marionette_harness/marionette_test/__init__.py
new file mode 100644
index 0000000000..436a282f26
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/marionette_test/__init__.py
@@ -0,0 +1,24 @@
+# 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/.
+
+__version__ = "3.1.0"
+
+from unittest.case import SkipTest, skip
+
+from .decorators import (
+ parameterized,
+ run_if_manage_instance,
+ skip_if_chrome,
+ skip_if_desktop,
+ skip_unless_browser_pref,
+ skip_unless_protocol,
+ with_parameters,
+)
+from .testcases import (
+ CommonTestCase,
+ MarionetteTestCase,
+ MetaParameterized,
+ expectedFailure,
+ unexpectedSuccess,
+)
diff --git a/testing/marionette/harness/marionette_harness/marionette_test/decorators.py b/testing/marionette/harness/marionette_harness/marionette_test/decorators.py
new file mode 100644
index 0000000000..cc3aa091d8
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/marionette_test/decorators.py
@@ -0,0 +1,194 @@
+# 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 functools
+import types
+from unittest.case import SkipTest
+
+
+def parameterized(func_suffix, *args, **kwargs):
+ r"""Decorator which generates methods given a base method and some data.
+
+ **func_suffix** is used as a suffix for the new created method and must be
+ unique given a base method. if **func_suffix** countains characters that
+ are not allowed in normal python function name, these characters will be
+ replaced with "_".
+
+ This decorator can be used more than once on a single base method. The class
+ must have a metaclass of :class:`MetaParameterized`.
+
+ Example::
+
+ # This example will generate two methods:
+ #
+ # - MyTestCase.test_it_1
+ # - MyTestCase.test_it_2
+ #
+ class MyTestCase(MarionetteTestCase):
+ @parameterized("1", 5, named='name')
+ @parameterized("2", 6, named='name2')
+ def test_it(self, value, named=None):
+ print value, named
+
+ :param func_suffix: will be used as a suffix for the new method
+ :param \*args: arguments to pass to the new method
+ :param \*\*kwargs: named arguments to pass to the new method
+ """
+
+ def wrapped(func):
+ if not hasattr(func, "metaparameters"):
+ func.metaparameters = []
+ func.metaparameters.append((func_suffix, args, kwargs))
+ return func
+
+ return wrapped
+
+
+def run_if_manage_instance(reason):
+ """Decorator which runs a test if Marionette manages the application instance."""
+
+ def decorator(test_item):
+ if not isinstance(test_item, types.FunctionType):
+ raise Exception("Decorator only supported for functions")
+
+ @functools.wraps(test_item)
+ def skip_wrapper(self, *args, **kwargs):
+ if self.marionette.instance is None:
+ raise SkipTest(reason)
+ return test_item(self, *args, **kwargs)
+
+ return skip_wrapper
+
+ return decorator
+
+
+def skip_if_chrome(reason):
+ """Decorator which skips a test if chrome context is active."""
+
+ def decorator(test_item):
+ if not isinstance(test_item, types.FunctionType):
+ raise Exception("Decorator only supported for functions")
+
+ @functools.wraps(test_item)
+ def skip_wrapper(self, *args, **kwargs):
+ if self.marionette._send_message("getContext", key="value") == "chrome":
+ raise SkipTest(reason)
+ return test_item(self, *args, **kwargs)
+
+ return skip_wrapper
+
+ return decorator
+
+
+def skip_if_desktop(reason):
+ """Decorator which skips a test if run on desktop."""
+
+ def decorator(test_item):
+ if not isinstance(test_item, types.FunctionType):
+ raise Exception("Decorator only supported for functions")
+
+ @functools.wraps(test_item)
+ def skip_wrapper(self, *args, **kwargs):
+ if self.marionette.session_capabilities.get("browserName") == "firefox":
+ raise SkipTest(reason)
+ return test_item(self, *args, **kwargs)
+
+ return skip_wrapper
+
+ return decorator
+
+
+def skip_unless_browser_pref(reason, pref, predicate=bool):
+ """Decorator which skips a test based on the value of a browser preference.
+
+ :param reason: Message describing why the test need to be skipped.
+ :param pref: the preference name
+ :param predicate: a function that should return false to skip the test.
+ The function takes one parameter, the preference value.
+ Defaults to the python built-in bool function.
+
+ Note that the preference must exist, else a failure is raised.
+
+ Example: ::
+
+ class TestSomething(MarionetteTestCase):
+ @skip_unless_browser_pref("Sessionstore needs to be enabled for crashes",
+ "browser.sessionstore.resume_from_crash",
+ lambda value: value is True,
+ )
+ def test_foo(self):
+ pass # test implementation here
+
+ """
+
+ def decorator(test_item):
+ if not isinstance(test_item, types.FunctionType):
+ raise Exception("Decorator only supported for functions")
+ if not callable(predicate):
+ raise ValueError("predicate must be callable")
+
+ @functools.wraps(test_item)
+ def skip_wrapper(self, *args, **kwargs):
+ value = self.marionette.get_pref(pref)
+ if value is None:
+ self.fail("No such browser preference: {0!r}".format(pref))
+ if not predicate(value):
+ raise SkipTest(reason)
+ return test_item(self, *args, **kwargs)
+
+ return skip_wrapper
+
+ return decorator
+
+
+def skip_unless_protocol(reason, predicate):
+ """Decorator which skips a test if the predicate does not match the current protocol level."""
+
+ def decorator(test_item):
+ if not isinstance(test_item, types.FunctionType):
+ raise Exception("Decorator only supported for functions")
+ if not callable(predicate):
+ raise ValueError("predicate must be callable")
+
+ @functools.wraps(test_item)
+ def skip_wrapper(self, *args, **kwargs):
+ level = self.marionette.client.protocol
+ if not predicate(level):
+ raise SkipTest(reason)
+ return test_item(self, *args, **kwargs)
+
+ return skip_wrapper
+
+ return decorator
+
+
+def with_parameters(parameters):
+ """Decorator which generates methods given a base method and some data.
+
+ Acts like :func:`parameterized`, but define all methods in one call.
+
+ Example::
+
+ # This example will generate two methods:
+ #
+ # - MyTestCase.test_it_1
+ # - MyTestCase.test_it_2
+ #
+
+ DATA = [("1", [5], {'named':'name'}), ("2", [6], {'named':'name2'})]
+
+ class MyTestCase(MarionetteTestCase):
+ @with_parameters(DATA)
+ def test_it(self, value, named=None):
+ print value, named
+
+ :param parameters: list of tuples (**func_suffix**, **args**, **kwargs**)
+ defining parameters like in :func:`todo`.
+ """
+
+ def wrapped(func):
+ func.metaparameters = parameters
+ return func
+
+ return wrapped
diff --git a/testing/marionette/harness/marionette_harness/marionette_test/testcases.py b/testing/marionette/harness/marionette_harness/marionette_test/testcases.py
new file mode 100644
index 0000000000..009e701f2d
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/marionette_test/testcases.py
@@ -0,0 +1,420 @@
+# 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 re
+import sys
+import time
+import unittest
+import warnings
+import weakref
+from unittest.case import SkipTest
+
+import six
+from marionette_driver.errors import TimeoutException, UnresponsiveInstanceException
+from mozfile import load_source
+from mozlog import get_default_logger
+
+
+# With Python 3 both expectedFailure and unexpectedSuccess are
+# available in unittest/case.py but won't work here because both
+# do not inherit from BaseException. And that's currently needed
+# in our custom test status handling in `run()`.
+class expectedFailure(Exception):
+ """
+ Raise this when a test is expected to fail.
+
+ This is an implementation detail.
+ """
+
+ def __init__(self, exc_info):
+ super(expectedFailure, self).__init__()
+ self.exc_info = exc_info
+
+
+class unexpectedSuccess(Exception):
+ """
+ The test was supposed to fail, but it didn't!
+ """
+
+ pass
+
+
+def _wraps_parameterized(func, func_suffix, args, kwargs):
+ """Internal: Decorator used in class MetaParameterized."""
+
+ def wrapper(self):
+ return func(self, *args, **kwargs)
+
+ wrapper.__name__ = func.__name__ + "_" + str(func_suffix)
+ wrapper.__doc__ = "[{0}] {1}".format(func_suffix, func.__doc__)
+ return wrapper
+
+
+class MetaParameterized(type):
+ """
+ A metaclass that allow a class to use decorators.
+
+ It can be used like :func:`parameterized`
+ or :func:`with_parameters` to generate new methods.
+ """
+
+ RE_ESCAPE_BAD_CHARS = re.compile(r"[\.\(\) -/]")
+
+ def __new__(cls, name, bases, attrs):
+ for k, v in list(attrs.items()):
+ if callable(v) and hasattr(v, "metaparameters"):
+ for func_suffix, args, kwargs in v.metaparameters:
+ func_suffix = cls.RE_ESCAPE_BAD_CHARS.sub("_", func_suffix)
+ wrapper = _wraps_parameterized(v, func_suffix, args, kwargs)
+ if wrapper.__name__ in attrs:
+ raise KeyError(
+ "{0} is already a defined method on {1}".format(
+ wrapper.__name__, name
+ )
+ )
+ attrs[wrapper.__name__] = wrapper
+ del attrs[k]
+
+ return type.__new__(cls, name, bases, attrs)
+
+
+@six.add_metaclass(MetaParameterized)
+class CommonTestCase(unittest.TestCase):
+ match_re = None
+ failureException = AssertionError
+ pydebugger = None
+
+ def __init__(self, methodName, marionette_weakref, fixtures, **kwargs):
+ super(CommonTestCase, self).__init__(methodName)
+ self.methodName = methodName
+
+ self._marionette_weakref = marionette_weakref
+ self.fixtures = fixtures
+
+ self.duration = 0
+ self.start_time = 0
+ self.expected = kwargs.pop("expected", "pass")
+ self.logger = get_default_logger()
+
+ def _enter_pm(self):
+ if self.pydebugger:
+ self.pydebugger.post_mortem(sys.exc_info()[2])
+
+ def _addSkip(self, result, reason):
+ addSkip = getattr(result, "addSkip", None)
+ if addSkip is not None:
+ addSkip(self, reason)
+ else:
+ warnings.warn(
+ "TestResult has no addSkip method, skips not reported",
+ RuntimeWarning,
+ 2,
+ )
+ result.addSuccess(self)
+
+ def assertRaisesRegxp(
+ self, expected_exception, expected_regexp, callable_obj=None, *args, **kwargs
+ ):
+ return six.assertRaisesRegex(
+ self,
+ expected_exception,
+ expected_regexp,
+ callable_obj=None,
+ *args,
+ **kwargs
+ )
+
+ def run(self, result=None):
+ # Bug 967566 suggests refactoring run, which would hopefully
+ # mean getting rid of this inner function, which only sits
+ # here to reduce code duplication:
+ def expected_failure(result, exc_info):
+ addExpectedFailure = getattr(result, "addExpectedFailure", None)
+ if addExpectedFailure is not None:
+ addExpectedFailure(self, exc_info)
+ else:
+ warnings.warn(
+ "TestResult has no addExpectedFailure method, "
+ "reporting as passes",
+ RuntimeWarning,
+ )
+ result.addSuccess(self)
+
+ self.start_time = time.time()
+ orig_result = result
+ if result is None:
+ result = self.defaultTestResult()
+ startTestRun = getattr(result, "startTestRun", None)
+ if startTestRun is not None:
+ startTestRun()
+
+ result.startTest(self)
+
+ testMethod = getattr(self, self._testMethodName)
+ if getattr(self.__class__, "__unittest_skip__", False) or getattr(
+ testMethod, "__unittest_skip__", False
+ ):
+ # If the class or method was skipped.
+ try:
+ skip_why = getattr(
+ self.__class__, "__unittest_skip_why__", ""
+ ) or getattr(testMethod, "__unittest_skip_why__", "")
+ self._addSkip(result, skip_why)
+ finally:
+ result.stopTest(self)
+ self.stop_time = time.time()
+ return
+ try:
+ success = False
+ try:
+ if self.expected == "fail":
+ try:
+ self.setUp()
+ except Exception:
+ raise expectedFailure(sys.exc_info())
+ else:
+ self.setUp()
+ except SkipTest as e:
+ self._addSkip(result, str(e))
+ except (KeyboardInterrupt, UnresponsiveInstanceException):
+ raise
+ except expectedFailure as e:
+ expected_failure(result, e.exc_info)
+ except Exception:
+ self._enter_pm()
+ result.addError(self, sys.exc_info())
+ else:
+ try:
+ if self.expected == "fail":
+ try:
+ testMethod()
+ except Exception:
+ raise expectedFailure(sys.exc_info())
+ raise unexpectedSuccess
+ else:
+ testMethod()
+ except self.failureException:
+ self._enter_pm()
+ result.addFailure(self, sys.exc_info())
+ except (KeyboardInterrupt, UnresponsiveInstanceException):
+ raise
+ except expectedFailure as e:
+ expected_failure(result, e.exc_info)
+ except unexpectedSuccess:
+ addUnexpectedSuccess = getattr(result, "addUnexpectedSuccess", None)
+ if addUnexpectedSuccess is not None:
+ addUnexpectedSuccess(self)
+ else:
+ warnings.warn(
+ "TestResult has no addUnexpectedSuccess method, "
+ "reporting as failures",
+ RuntimeWarning,
+ )
+ result.addFailure(self, sys.exc_info())
+ except SkipTest as e:
+ self._addSkip(result, str(e))
+ except Exception:
+ self._enter_pm()
+ result.addError(self, sys.exc_info())
+ else:
+ success = True
+ try:
+ if self.expected == "fail":
+ try:
+ self.tearDown()
+ except Exception:
+ raise expectedFailure(sys.exc_info())
+ else:
+ self.tearDown()
+ except (KeyboardInterrupt, UnresponsiveInstanceException):
+ raise
+ except expectedFailure as e:
+ expected_failure(result, e.exc_info)
+ except Exception:
+ self._enter_pm()
+ result.addError(self, sys.exc_info())
+ success = False
+ # Here we could handle doCleanups() instead of calling cleanTest directly
+ self.cleanTest()
+
+ if success:
+ result.addSuccess(self)
+
+ finally:
+ result.stopTest(self)
+ if orig_result is None:
+ stopTestRun = getattr(result, "stopTestRun", None)
+ if stopTestRun is not None:
+ stopTestRun()
+
+ @classmethod
+ def match(cls, filename):
+ """Determine if the specified filename should be handled by this test class.
+
+ This is done by looking for a match for the filename using cls.match_re.
+ """
+ if not cls.match_re:
+ return False
+ m = cls.match_re.match(filename)
+ return m is not None
+
+ @classmethod
+ def add_tests_to_suite(
+ cls,
+ mod_name,
+ filepath,
+ suite,
+ testloader,
+ marionette,
+ fixtures,
+ testvars,
+ **kwargs
+ ):
+ """Add all the tests in the specified file to the specified suite."""
+ raise NotImplementedError
+
+ @property
+ def test_name(self):
+ rel_path = None
+ if os.path.exists(self.filepath):
+ rel_path = self._fix_test_path(self.filepath)
+
+ return "{0} {1}.{2}".format(
+ rel_path, self.__class__.__name__, self._testMethodName
+ )
+
+ def id(self):
+ # TBPL starring requires that the "test name" field of a failure message
+ # not differ over time. The test name to be used is passed to
+ # mozlog via the test id, so this is overriden to maintain
+ # consistency.
+ return self.test_name
+
+ def setUp(self):
+ # Convert the marionette weakref to an object, just for the
+ # duration of the test; this is deleted in tearDown() to prevent
+ # a persistent circular reference which in turn would prevent
+ # proper garbage collection.
+ self.start_time = time.time()
+ self.marionette = self._marionette_weakref()
+ if self.marionette.session is None:
+ self.marionette.start_session()
+ self.marionette.timeout.reset()
+
+ super(CommonTestCase, self).setUp()
+
+ def cleanTest(self):
+ self._delete_session()
+
+ def _delete_session(self):
+ if hasattr(self, "start_time"):
+ self.duration = time.time() - self.start_time
+ if self.marionette.session is not None:
+ try:
+ self.marionette.delete_session()
+ except IOError:
+ # Gecko has crashed?
+ pass
+ self.marionette = None
+
+ def _fix_test_path(self, path):
+ """Normalize a logged test path from the test package."""
+ test_path_prefixes = [
+ "tests{}".format(os.path.sep),
+ ]
+
+ path = os.path.relpath(path)
+ for prefix in test_path_prefixes:
+ if path.startswith(prefix):
+ path = path[len(prefix) :]
+ break
+ path = path.replace("\\", "/")
+
+ return path
+
+
+class MarionetteTestCase(CommonTestCase):
+ match_re = re.compile(r"test_(.*)\.py$")
+
+ def __init__(
+ self, marionette_weakref, fixtures, methodName="runTest", filepath="", **kwargs
+ ):
+ self.filepath = filepath
+ self.testvars = kwargs.pop("testvars", None)
+
+ super(MarionetteTestCase, self).__init__(
+ methodName,
+ marionette_weakref=marionette_weakref,
+ fixtures=fixtures,
+ **kwargs
+ )
+
+ @classmethod
+ def add_tests_to_suite(
+ cls,
+ mod_name,
+ filepath,
+ suite,
+ testloader,
+ marionette,
+ fixtures,
+ testvars,
+ **kwargs
+ ):
+ # since load_source caches modules, if a module is loaded with the same
+ # name as another one the module would just be reloaded.
+ #
+ # We may end up by finding too many test in a module then since reload()
+ # only update the module dict (so old keys are still there!) see
+ # https://docs.python.org/2/library/functions.html#reload
+ #
+ # we get rid of that by removing the module from sys.modules, so we
+ # ensure that it will be fully loaded by the imp.load_source call.
+
+ if mod_name in sys.modules:
+ del sys.modules[mod_name]
+
+ test_mod = load_source(mod_name, filepath)
+
+ for name in dir(test_mod):
+ obj = getattr(test_mod, name)
+ if isinstance(obj, six.class_types) and issubclass(obj, unittest.TestCase):
+ testnames = testloader.getTestCaseNames(obj)
+ for testname in testnames:
+ suite.addTest(
+ obj(
+ weakref.ref(marionette),
+ fixtures,
+ methodName=testname,
+ filepath=filepath,
+ testvars=testvars,
+ **kwargs
+ )
+ )
+
+ def setUp(self):
+ super(MarionetteTestCase, self).setUp()
+ self.marionette.test_name = self.test_name
+
+ def tearDown(self):
+ # In the case no session is active (eg. the application was quit), start
+ # a new session for clean-up steps.
+ if not self.marionette.session:
+ self.marionette.start_session()
+
+ self.marionette.test_name = None
+
+ super(MarionetteTestCase, self).tearDown()
+
+ def wait_for_condition(self, method, timeout=30):
+ timeout = float(timeout) + time.time()
+ while time.time() < timeout:
+ value = method(self.marionette)
+ if value:
+ return value
+ time.sleep(0.5)
+ else:
+ raise TimeoutException("wait_for_condition timed out")
diff --git a/testing/marionette/harness/marionette_harness/runner/__init__.py b/testing/marionette/harness/marionette_harness/runner/__init__.py
new file mode 100644
index 0000000000..2fdac637d3
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/runner/__init__.py
@@ -0,0 +1,16 @@
+# 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 .base import (
+ BaseMarionetteArguments,
+ BaseMarionetteTestRunner,
+ Marionette,
+ MarionetteTest,
+ MarionetteTestResult,
+ MarionetteTextTestRunner,
+ TestManifest,
+ TestResult,
+ TestResultCollection,
+)
+from .mixins import WindowManagerMixin
diff --git a/testing/marionette/harness/marionette_harness/runner/base.py b/testing/marionette/harness/marionette_harness/runner/base.py
new file mode 100644
index 0000000000..b5ddc2d788
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/runner/base.py
@@ -0,0 +1,1265 @@
+# 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 random
+import re
+import socket
+import sys
+import time
+import traceback
+import unittest
+from argparse import ArgumentParser
+from collections import defaultdict
+from copy import deepcopy
+
+import mozinfo
+import moznetwork
+import mozprofile
+import mozversion
+import six
+from manifestparser import TestManifest
+from manifestparser.filters import tags
+from marionette_driver.marionette import Marionette
+from moztest.adapters.unit import StructuredTestResult, StructuredTestRunner
+from moztest.results import TestResult, TestResultCollection, relevant_line
+from six import MAXSIZE, reraise
+
+from . import serve
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+def update_mozinfo(path=None):
+ """Walk up directories to find mozinfo.json and update the info."""
+ path = path or here
+ dirs = set()
+ while path != os.path.expanduser("~"):
+ if path in dirs:
+ break
+ dirs.add(path)
+ path = os.path.split(path)[0]
+
+ return mozinfo.find_and_update_from_json(*dirs)
+
+
+class MarionetteTest(TestResult):
+ @property
+ def test_name(self):
+ if self.test_class is not None:
+ return "{0}.py {1}.{2}".format(
+ self.test_class.split(".")[0], self.test_class, self.name
+ )
+ else:
+ return self.name
+
+
+class MarionetteTestResult(StructuredTestResult, TestResultCollection):
+ resultClass = MarionetteTest
+
+ def __init__(self, *args, **kwargs):
+ self.marionette = kwargs.pop("marionette")
+ TestResultCollection.__init__(self, "MarionetteTest")
+ self.passed = 0
+ self.testsRun = 0
+ self.result_modifiers = [] # used by mixins to modify the result
+ StructuredTestResult.__init__(self, *args, **kwargs)
+
+ @property
+ def skipped(self):
+ return [t for t in self if t.result == "SKIPPED"]
+
+ @skipped.setter
+ def skipped(self, value):
+ pass
+
+ @property
+ def expectedFailures(self):
+ return [t for t in self if t.result == "KNOWN-FAIL"]
+
+ @expectedFailures.setter
+ def expectedFailures(self, value):
+ pass
+
+ @property
+ def unexpectedSuccesses(self):
+ return [t for t in self if t.result == "UNEXPECTED-PASS"]
+
+ @unexpectedSuccesses.setter
+ def unexpectedSuccesses(self, value):
+ pass
+
+ @property
+ def tests_passed(self):
+ return [t for t in self if t.result == "PASS"]
+
+ @property
+ def errors(self):
+ return [t for t in self if t.result == "ERROR"]
+
+ @errors.setter
+ def errors(self, value):
+ pass
+
+ @property
+ def failures(self):
+ return [t for t in self if t.result == "UNEXPECTED-FAIL"]
+
+ @failures.setter
+ def failures(self, value):
+ pass
+
+ @property
+ def duration(self):
+ if self.stop_time:
+ return self.stop_time - self.start_time
+ else:
+ return 0
+
+ def add_test_result(
+ self,
+ test,
+ result_expected="PASS",
+ result_actual="PASS",
+ output="",
+ context=None,
+ **kwargs
+ ):
+ def get_class(test):
+ return test.__class__.__module__ + "." + test.__class__.__name__
+
+ name = str(test).split()[0]
+ test_class = get_class(test)
+ if hasattr(test, "jsFile"):
+ name = os.path.basename(test.jsFile)
+ test_class = None
+
+ t = self.resultClass(
+ name=name,
+ test_class=test_class,
+ time_start=test.start_time,
+ result_expected=result_expected,
+ context=context,
+ **kwargs
+ )
+ # call any registered result modifiers
+ for modifier in self.result_modifiers:
+ result_expected, result_actual, output, context = modifier(
+ t, result_expected, result_actual, output, context
+ )
+ t.finish(
+ result_actual,
+ time_end=time.time() if test.start_time else 0,
+ reason=relevant_line(output),
+ output=output,
+ )
+ self.append(t)
+
+ def addError(self, test, err):
+ self.add_test_result(
+ test, output=self._exc_info_to_string(err, test), result_actual="ERROR"
+ )
+ super(MarionetteTestResult, self).addError(test, err)
+
+ def addFailure(self, test, err):
+ self.add_test_result(
+ test,
+ output=self._exc_info_to_string(err, test),
+ result_actual="UNEXPECTED-FAIL",
+ )
+ super(MarionetteTestResult, self).addFailure(test, err)
+
+ def addSuccess(self, test):
+ self.passed += 1
+ self.add_test_result(test, result_actual="PASS")
+ super(MarionetteTestResult, self).addSuccess(test)
+
+ def addExpectedFailure(self, test, err):
+ """Called when an expected failure/error occured."""
+ self.add_test_result(
+ test, output=self._exc_info_to_string(err, test), result_actual="KNOWN-FAIL"
+ )
+ super(MarionetteTestResult, self).addExpectedFailure(test, err)
+
+ def addUnexpectedSuccess(self, test):
+ """Called when a test was expected to fail, but succeed."""
+ self.add_test_result(test, result_actual="UNEXPECTED-PASS")
+ super(MarionetteTestResult, self).addUnexpectedSuccess(test)
+
+ def addSkip(self, test, reason):
+ self.add_test_result(test, output=reason, result_actual="SKIPPED")
+ super(MarionetteTestResult, self).addSkip(test, reason)
+
+ def getInfo(self, test):
+ return test.test_name
+
+ def getDescription(self, test):
+ doc_first_line = test.shortDescription()
+ if self.descriptions and doc_first_line:
+ return "\n".join((str(test), doc_first_line))
+ else:
+ desc = str(test)
+ return desc
+
+ def printLogs(self, test):
+ for testcase in test._tests:
+ if hasattr(testcase, "loglines") and testcase.loglines:
+ # Don't dump loglines to the console if they only contain
+ # TEST-START and TEST-END.
+ skip_log = True
+ for line in testcase.loglines:
+ str_line = " ".join(line)
+ if "TEST-END" not in str_line and "TEST-START" not in str_line:
+ skip_log = False
+ break
+ if skip_log:
+ return
+ self.logger.info("START LOG:")
+ for line in testcase.loglines:
+ self.logger.info(" ".join(line).encode("ascii", "replace"))
+ self.logger.info("END LOG:")
+
+ def stopTest(self, *args, **kwargs):
+ unittest._TextTestResult.stopTest(self, *args, **kwargs)
+ if self.marionette.check_for_crash():
+ # this tells unittest.TestSuite not to continue running tests
+ self.shouldStop = True
+ test = next((a for a in args if isinstance(a, unittest.TestCase)), None)
+ if test:
+ self.addError(test, sys.exc_info())
+
+
+class MarionetteTextTestRunner(StructuredTestRunner):
+ resultclass = MarionetteTestResult
+
+ def __init__(self, **kwargs):
+ self.marionette = kwargs.pop("marionette")
+ self.capabilities = kwargs.pop("capabilities")
+
+ StructuredTestRunner.__init__(self, **kwargs)
+
+ def _makeResult(self):
+ return self.resultclass(
+ self.stream,
+ self.descriptions,
+ self.verbosity,
+ marionette=self.marionette,
+ logger=self.logger,
+ result_callbacks=self.result_callbacks,
+ )
+
+ def run(self, test):
+ result = super(MarionetteTextTestRunner, self).run(test)
+ result.printLogs(test)
+ return result
+
+
+class BaseMarionetteArguments(ArgumentParser):
+ def __init__(self, **kwargs):
+ ArgumentParser.__init__(self, **kwargs)
+
+ def dir_path(path):
+ path = os.path.abspath(os.path.expanduser(path))
+ if not os.access(path, os.F_OK):
+ os.makedirs(path)
+ return path
+
+ self.argument_containers = []
+ self.add_argument(
+ "tests",
+ nargs="*",
+ default=[],
+ help="Tests to run. "
+ "One or more paths to test files (Python or JS), "
+ "manifest files (.toml) or directories. "
+ "When a directory is specified, "
+ "all test files in the directory will be run.",
+ )
+ self.add_argument(
+ "--binary",
+ help="path to gecko executable to launch before running the test",
+ )
+ self.add_argument(
+ "--address", help="host:port of running Gecko instance to connect to"
+ )
+ self.add_argument(
+ "--emulator",
+ action="store_true",
+ help="If no --address is given, then the harness will launch an "
+ "emulator. (See Remote options group.) "
+ "If --address is given, then the harness assumes you are "
+ "running an emulator already, and will launch gecko app "
+ "on that emulator.",
+ )
+ self.add_argument(
+ "--app", help="application to use. see marionette_driver.geckoinstance"
+ )
+ self.add_argument(
+ "--app-arg",
+ dest="app_args",
+ action="append",
+ default=[],
+ help="specify a command line argument to be passed onto the application",
+ )
+ self.add_argument(
+ "--profile",
+ help="profile to use when launching the gecko process. If not passed, "
+ "then a profile will be constructed and used",
+ type=dir_path,
+ )
+ self.add_argument(
+ "--setpref",
+ action="append",
+ metavar="PREF=VALUE",
+ dest="prefs_args",
+ help="set a browser preference; repeat for multiple preferences.",
+ )
+ self.add_argument(
+ "--preferences",
+ action="append",
+ dest="prefs_files",
+ help="read preferences from a JSON or TOML file. For TOML, use "
+ "'file.toml:section' to specify a particular section.",
+ )
+ self.add_argument(
+ "--addon",
+ action="append",
+ dest="addons",
+ help="addon to install; repeat for multiple addons.",
+ )
+ self.add_argument(
+ "--repeat", type=int, help="number of times to repeat the test(s)"
+ )
+ self.add_argument(
+ "--run-until-failure",
+ action="store_true",
+ help="Run tests repeatedly and stop on the first time a test fails. "
+ "Default cap is 30 runs, which can be overwritten "
+ "with the --repeat parameter.",
+ )
+ self.add_argument(
+ "--testvars",
+ action="append",
+ help="path to a json file with any test data required",
+ )
+ self.add_argument(
+ "--symbols-path",
+ help="absolute path to directory containing breakpad symbols, or the "
+ "url of a zip file containing symbols",
+ )
+ self.add_argument(
+ "--socket-timeout",
+ type=float,
+ default=Marionette.DEFAULT_SOCKET_TIMEOUT,
+ help="Set the global timeout for marionette socket operations."
+ " Default: %(default)ss.",
+ )
+ self.add_argument(
+ "--startup-timeout",
+ type=int,
+ default=Marionette.DEFAULT_STARTUP_TIMEOUT,
+ help="the max number of seconds to wait for a Marionette connection "
+ "after launching a binary. Default: %(default)ss.",
+ )
+ self.add_argument(
+ "--shuffle",
+ action="store_true",
+ default=False,
+ help="run tests in a random order",
+ )
+ self.add_argument(
+ "--shuffle-seed",
+ type=int,
+ default=random.randint(0, MAXSIZE),
+ help="Use given seed to shuffle tests",
+ )
+ self.add_argument(
+ "--total-chunks",
+ type=int,
+ help="how many chunks to split the tests up into",
+ )
+ self.add_argument("--this-chunk", type=int, help="which chunk to run")
+ self.add_argument(
+ "--server-root",
+ help="url to a webserver or path to a document root from which content "
+ "resources are served (default: {}).".format(
+ os.path.join(os.path.dirname(here), "www")
+ ),
+ )
+ self.add_argument(
+ "--gecko-log",
+ help="Define the path to store log file. If the path is"
+ " a directory, the real log file will be created"
+ " given the format gecko-(timestamp).log. If it is"
+ " a file, if will be used directly. '-' may be passed"
+ " to write to stdout. Default: './gecko.log'",
+ )
+ self.add_argument(
+ "--logger-name",
+ default="Marionette-based Tests",
+ help="Define the name to associate with the logger used",
+ )
+ self.add_argument(
+ "--jsdebugger",
+ action="store_true",
+ default=False,
+ help="Enable the jsdebugger for marionette javascript.",
+ )
+ self.add_argument(
+ "--pydebugger",
+ help="Enable python post-mortem debugger when a test fails."
+ " Pass in the debugger you want to use, eg pdb or ipdb.",
+ )
+ self.add_argument(
+ "--disable-fission",
+ action="store_true",
+ dest="disable_fission",
+ default=False,
+ help="Disable Fission (site isolation) in Gecko.",
+ )
+ self.add_argument(
+ "-z",
+ "--headless",
+ action="store_true",
+ dest="headless",
+ default=os.environ.get("MOZ_HEADLESS", False),
+ help="Run tests in headless mode.",
+ )
+ self.add_argument(
+ "--tag",
+ action="append",
+ dest="test_tags",
+ default=None,
+ help="Filter out tests that don't have the given tag. Can be "
+ "used multiple times in which case the test must contain "
+ "at least one of the given tags.",
+ )
+ self.add_argument(
+ "--workspace",
+ action="store",
+ default=None,
+ help="Path to directory for Marionette output. "
+ "(Default: .) (Default profile dest: TMP)",
+ type=dir_path,
+ )
+ self.add_argument(
+ "-v",
+ "--verbose",
+ action="count",
+ help="Increase verbosity to include debug messages with -v, "
+ "and trace messages with -vv.",
+ )
+ self.register_argument_container(RemoteMarionetteArguments())
+
+ def register_argument_container(self, container):
+ group = self.add_argument_group(container.name)
+
+ for cli, kwargs in container.args:
+ group.add_argument(*cli, **kwargs)
+
+ self.argument_containers.append(container)
+
+ def parse_known_args(self, args=None, namespace=None):
+ args, remainder = ArgumentParser.parse_known_args(self, args, namespace)
+ for container in self.argument_containers:
+ if hasattr(container, "parse_args_handler"):
+ container.parse_args_handler(args)
+ return (args, remainder)
+
+ def _get_preferences(self, prefs_files, prefs_args):
+ """Return user defined profile preferences as a dict."""
+ # object that will hold the preferences
+ prefs = mozprofile.prefs.Preferences()
+
+ # add preferences files
+ if prefs_files:
+ for prefs_file in prefs_files:
+ prefs.add_file(prefs_file)
+
+ separator = "="
+ cli_prefs = []
+ if prefs_args:
+ misformatted = []
+ for pref in prefs_args:
+ if separator not in pref:
+ misformatted.append(pref)
+ else:
+ cli_prefs.append(pref.split(separator, 1))
+ if misformatted:
+ self._print_message(
+ "Warning: Ignoring preferences not in key{}value format: {}\n".format(
+ separator, ", ".join(misformatted)
+ )
+ )
+ # string preferences
+ prefs.add(cli_prefs, cast=True)
+
+ return dict(prefs())
+
+ def verify_usage(self, args):
+ if not args.tests:
+ self.error(
+ "You must specify one or more test files, manifests, or directories."
+ )
+
+ missing_tests = [path for path in args.tests if not os.path.exists(path)]
+ if missing_tests:
+ self.error(
+ "Test file(s) not found: " + " ".join([path for path in missing_tests])
+ )
+
+ if not args.address and not args.binary and not args.emulator:
+ self.error("You must specify --binary, or --address, or --emulator")
+
+ if args.repeat is not None and args.repeat < 0:
+ self.error("The value of --repeat has to be equal or greater than 0.")
+
+ if args.total_chunks is not None and args.this_chunk is None:
+ self.error("You must specify which chunk to run.")
+
+ if args.this_chunk is not None and args.total_chunks is None:
+ self.error("You must specify how many chunks to split the tests into.")
+
+ if args.total_chunks is not None:
+ if not 1 < args.total_chunks:
+ self.error("Total chunks must be greater than 1.")
+ if not 1 <= args.this_chunk <= args.total_chunks:
+ self.error(
+ "Chunk to run must be between 1 and {}.".format(args.total_chunks)
+ )
+
+ if args.jsdebugger:
+ args.app_args.append("-jsdebugger")
+ args.socket_timeout = None
+
+ args.prefs = self._get_preferences(args.prefs_files, args.prefs_args)
+
+ for container in self.argument_containers:
+ if hasattr(container, "verify_usage_handler"):
+ container.verify_usage_handler(args)
+
+ return args
+
+
+class RemoteMarionetteArguments(object):
+ name = "Remote (Emulator/Device)"
+ args = [
+ [
+ ["--emulator-binary"],
+ {
+ "help": "Path to emulator binary. By default mozrunner uses `which emulator`",
+ "dest": "emulator_bin",
+ },
+ ],
+ [
+ ["--adb"],
+ {
+ "help": "Path to the adb. By default mozrunner uses `which adb`",
+ "dest": "adb_path",
+ },
+ ],
+ [
+ ["--avd"],
+ {
+ "help": (
+ "Name of an AVD available in your environment."
+ "See mozrunner.FennecEmulatorRunner"
+ ),
+ },
+ ],
+ [
+ ["--avd-home"],
+ {
+ "help": "Path to avd parent directory",
+ },
+ ],
+ [
+ ["--device"],
+ {
+ "help": (
+ "Serial ID to connect to as seen in `adb devices`,"
+ "e.g emulator-5444"
+ ),
+ "dest": "device_serial",
+ },
+ ],
+ [
+ ["--package"],
+ {
+ "help": "Name of Android package, e.g. org.mozilla.fennec",
+ "dest": "package_name",
+ },
+ ],
+ ]
+
+
+class Fixtures(object):
+ def where_is(self, uri, on="http"):
+ return serve.where_is(uri, on)
+
+
+class BaseMarionetteTestRunner(object):
+ textrunnerclass = MarionetteTextTestRunner
+ driverclass = Marionette
+
+ def __init__(
+ self,
+ address=None,
+ app=None,
+ app_args=None,
+ binary=None,
+ profile=None,
+ logger=None,
+ logdir=None,
+ repeat=None,
+ run_until_failure=None,
+ testvars=None,
+ symbols_path=None,
+ shuffle=False,
+ shuffle_seed=random.randint(0, MAXSIZE),
+ this_chunk=1,
+ total_chunks=1,
+ server_root=None,
+ gecko_log=None,
+ result_callbacks=None,
+ prefs=None,
+ test_tags=None,
+ socket_timeout=None,
+ startup_timeout=None,
+ addons=None,
+ workspace=None,
+ verbose=0,
+ emulator=False,
+ headless=False,
+ disable_fission=False,
+ **kwargs
+ ):
+ self._appName = None
+ self._capabilities = None
+ self._filename_pattern = None
+ self._version_info = {}
+
+ self.fixture_servers = {}
+ self.fixtures = Fixtures()
+ self.extra_kwargs = kwargs
+ self.test_kwargs = deepcopy(kwargs)
+ self.address = address
+ self.app = app
+ self.app_args = app_args or []
+ self.bin = binary
+ self.emulator = emulator
+ self.profile = profile
+ self.addons = addons
+ self.logger = logger
+ self.marionette = None
+ self.logdir = logdir
+ self.repeat = repeat or 0
+ self.run_until_failure = run_until_failure or False
+ self.symbols_path = symbols_path
+ self.socket_timeout = socket_timeout
+ self.startup_timeout = startup_timeout
+ self.shuffle = shuffle
+ self.shuffle_seed = shuffle_seed
+ self.server_root = server_root
+ self.this_chunk = this_chunk
+ self.total_chunks = total_chunks
+ self.mixin_run_tests = []
+ self.manifest_skipped_tests = []
+ self.tests = []
+ self.result_callbacks = result_callbacks or []
+ self.prefs = prefs or {}
+ self.test_tags = test_tags
+ self.workspace = workspace
+ # If no workspace is set, default location for gecko.log is .
+ # and default location for profile is TMP
+ self.workspace_path = workspace or os.getcwd()
+ self.verbose = verbose
+ self.headless = headless
+
+ self.prefs.update({"fission.autostart": not disable_fission})
+
+ # If no repeat has been set, default to 30 extra runs
+ if self.run_until_failure and repeat is None:
+ self.repeat = 30
+
+ def gather_debug(test, status):
+ # No screenshots and page source for skipped tests
+ if status == "SKIP":
+ return
+
+ rv = {}
+ marionette = test._marionette_weakref()
+
+ # In the event we're gathering debug without starting a session,
+ # skip marionette commands
+ if marionette.session is not None:
+ try:
+ with marionette.using_context(marionette.CONTEXT_CHROME):
+ rv["screenshot"] = marionette.screenshot()
+ with marionette.using_context(marionette.CONTEXT_CONTENT):
+ rv["source"] = marionette.page_source
+ except Exception as exc:
+ self.logger.warning(
+ "Failed to gather test failure debug: {}".format(exc)
+ )
+ return rv
+
+ self.result_callbacks.append(gather_debug)
+
+ # testvars are set up in self.testvars property
+ self._testvars = None
+ self.testvars_paths = testvars
+
+ self.test_handlers = []
+
+ self.reset_test_stats()
+
+ self.logger.info(
+ "Using workspace for temporary data: " '"{}"'.format(self.workspace_path)
+ )
+
+ if not gecko_log:
+ self.gecko_log = os.path.join(self.workspace_path or "", "gecko.log")
+ else:
+ self.gecko_log = gecko_log
+
+ self.results = []
+
+ @property
+ def filename_pattern(self):
+ if self._filename_pattern is None:
+ self._filename_pattern = re.compile("^test(((_.+?)+?\.((py))))$")
+
+ return self._filename_pattern
+
+ @property
+ def testvars(self):
+ if self._testvars is not None:
+ return self._testvars
+
+ self._testvars = {}
+
+ def update(d, u):
+ """Update a dictionary that may contain nested dictionaries."""
+ for k, v in six.iteritems(u):
+ o = d.get(k, {})
+ if isinstance(v, dict) and isinstance(o, dict):
+ d[k] = update(d.get(k, {}), v)
+ else:
+ d[k] = u[k]
+ return d
+
+ json_testvars = self._load_testvars()
+ for j in json_testvars:
+ self._testvars = update(self._testvars, j)
+ return self._testvars
+
+ def _load_testvars(self):
+ data = []
+ if self.testvars_paths is not None:
+ for path in list(self.testvars_paths):
+ path = os.path.abspath(os.path.expanduser(path))
+ if not os.path.exists(path):
+ raise IOError("--testvars file {} does not exist".format(path))
+ try:
+ with open(path) as f:
+ data.append(json.loads(f.read()))
+ except ValueError as e:
+ msg = "JSON file ({0}) is not properly formatted: {1}"
+ reraise(
+ ValueError,
+ ValueError(msg.format(os.path.abspath(path), e)),
+ sys.exc_info()[2],
+ )
+ return data
+
+ @property
+ def capabilities(self):
+ if self._capabilities:
+ return self._capabilities
+
+ self.marionette.start_session()
+ self._capabilities = self.marionette.session_capabilities
+ self.marionette.delete_session()
+ return self._capabilities
+
+ @property
+ def appName(self):
+ if self._appName:
+ return self._appName
+
+ self._appName = self.capabilities.get("browserName")
+ return self._appName
+
+ @property
+ def bin(self):
+ return self._bin
+
+ @bin.setter
+ def bin(self, path):
+ """Set binary and reset parts of runner accordingly.
+ Intended use: to change binary between calls to run_tests
+ """
+ self._bin = path
+ self.tests = []
+ self.cleanup()
+
+ @property
+ def version_info(self):
+ if not self._version_info:
+ try:
+ # TODO: Get version_info in Fennec case
+ self._version_info = mozversion.get_version(binary=self.bin)
+ except Exception:
+ self.logger.warning(
+ "Failed to retrieve version information for {}".format(self.bin)
+ )
+ return self._version_info
+
+ def reset_test_stats(self):
+ self.passed = 0
+ self.failed = 0
+ self.crashed = 0
+ self.unexpected_successes = 0
+ self.todo = 0
+ self.skipped = 0
+ self.failures = []
+
+ def _build_kwargs(self):
+ if self.logdir and not os.access(self.logdir, os.F_OK):
+ os.mkdir(self.logdir)
+
+ kwargs = {
+ "socket_timeout": self.socket_timeout,
+ "prefs": self.prefs,
+ "startup_timeout": self.startup_timeout,
+ "verbose": self.verbose,
+ "symbols_path": self.symbols_path,
+ }
+ if self.bin or self.emulator:
+ kwargs.update(
+ {
+ "host": "127.0.0.1",
+ "port": 2828,
+ "app": self.app,
+ "app_args": self.app_args,
+ "profile": self.profile,
+ "addons": self.addons,
+ "gecko_log": self.gecko_log,
+ # ensure Marionette class takes care of starting gecko instance
+ "bin": True,
+ }
+ )
+
+ if self.bin:
+ kwargs.update(
+ {
+ "bin": self.bin,
+ }
+ )
+
+ if self.emulator:
+ kwargs.update(
+ {
+ "avd_home": self.extra_kwargs.get("avd_home"),
+ "adb_path": self.extra_kwargs.get("adb_path"),
+ "emulator_binary": self.extra_kwargs.get("emulator_bin"),
+ "avd": self.extra_kwargs.get("avd"),
+ "package_name": self.extra_kwargs.get("package_name"),
+ }
+ )
+
+ if self.address:
+ host, port = self.address.split(":")
+ kwargs.update(
+ {
+ "host": host,
+ "port": int(port),
+ }
+ )
+ if self.emulator:
+ kwargs.update(
+ {
+ "connect_to_running_emulator": True,
+ }
+ )
+ if not self.bin and not self.emulator:
+ try:
+ # Establish a socket connection so we can vertify the data come back
+ connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ connection.connect((host, int(port)))
+ connection.close()
+ except Exception as e:
+ exc_cls, _, tb = sys.exc_info()
+ msg = "Connection attempt to {0}:{1} failed with error: {2}"
+ reraise(exc_cls, exc_cls(msg.format(host, port, e)), tb)
+ if self.workspace:
+ kwargs["workspace"] = self.workspace_path
+ if self.headless:
+ kwargs["headless"] = True
+
+ return kwargs
+
+ def record_crash(self):
+ crash = True
+ try:
+ crash = self.marionette.check_for_crash()
+ self.crashed += int(crash)
+ except Exception:
+ traceback.print_exc()
+ return crash
+
+ def _initialize_test_run(self, tests):
+ assert len(tests) > 0
+ assert len(self.test_handlers) > 0
+ self.reset_test_stats()
+
+ def _add_tests(self, tests):
+ for test in tests:
+ self.add_test(test)
+
+ invalid_tests = [
+ t["filepath"]
+ for t in self.tests
+ if not self._is_filename_valid(t["filepath"])
+ ]
+ if invalid_tests:
+ raise Exception(
+ "Test file names must be of the form "
+ "'test_something.py'."
+ " Invalid test names:\n {}".format("\n ".join(invalid_tests))
+ )
+
+ def _is_filename_valid(self, filename):
+ filename = os.path.basename(filename)
+ return self.filename_pattern.match(filename)
+
+ def _fix_test_path(self, path):
+ """Normalize a logged test path from the test package."""
+ test_path_prefixes = [
+ "tests{}".format(os.path.sep),
+ ]
+
+ path = os.path.relpath(path)
+ for prefix in test_path_prefixes:
+ if path.startswith(prefix):
+ path = path[len(prefix) :]
+ break
+ path = path.replace("\\", "/")
+
+ return path
+
+ def _log_skipped_tests(self):
+ for test in self.manifest_skipped_tests:
+ rel_path = None
+ if os.path.exists(test["path"]):
+ rel_path = self._fix_test_path(test["path"])
+
+ self.logger.test_start(rel_path)
+ self.logger.test_end(rel_path, "SKIP", message=test["disabled"])
+ self.todo += 1
+
+ def run_tests(self, tests):
+ start_time = time.time()
+ self._initialize_test_run(tests)
+
+ if self.marionette is None:
+ self.marionette = self.driverclass(**self._build_kwargs())
+ self.logger.info("Profile path is %s" % self.marionette.profile_path)
+
+ if len(self.fixture_servers) == 0 or any(
+ not server.is_alive for _, server in self.fixture_servers
+ ):
+ self.logger.info("Starting fixture servers")
+ self.fixture_servers = self.start_fixture_servers()
+ for url in serve.iter_url(self.fixture_servers):
+ self.logger.info("Fixture server listening on %s" % url)
+
+ # backwards compatibility
+ self.marionette.baseurl = serve.where_is("/")
+
+ self._add_tests(tests)
+
+ device_info = None
+ if self.marionette.instance and self.emulator:
+ try:
+ device_info = self.marionette.instance.runner.device.device.get_info()
+ except Exception:
+ self.logger.warning("Could not get device info", exc_info=True)
+
+ tests_by_group = defaultdict(list)
+ for test in self.tests:
+ group = self._fix_test_path(test["group"])
+ filepath = self._fix_test_path(test["filepath"])
+ tests_by_group[group].append(filepath)
+
+ self.logger.suite_start(
+ tests_by_group,
+ name="marionette-test",
+ version_info=self.version_info,
+ device_info=device_info,
+ )
+
+ if self.shuffle:
+ self.logger.info("Using shuffle seed: %d" % self.shuffle_seed)
+
+ self._log_skipped_tests()
+
+ interrupted = None
+ try:
+ repeat_index = 0
+ while repeat_index <= self.repeat:
+ if repeat_index > 0:
+ self.logger.info("\nREPEAT {}\n-------".format(repeat_index))
+ self.run_test_sets()
+ if self.run_until_failure and self.failed > 0:
+ break
+
+ repeat_index += 1
+
+ except KeyboardInterrupt:
+ # in case of KeyboardInterrupt during the test execution
+ # we want to display current test results.
+ # so we keep the exception to raise it later.
+ interrupted = sys.exc_info()
+ except Exception:
+ # For any other exception we return immediately and have to
+ # cleanup running processes
+ self.cleanup()
+ raise
+
+ try:
+ self._print_summary(tests)
+ self.record_crash()
+ self.elapsedtime = time.time() - start_time
+
+ for run_tests in self.mixin_run_tests:
+ run_tests(tests)
+
+ self.logger.suite_end()
+ except Exception:
+ # raise only the exception if we were not interrupted
+ if not interrupted:
+ raise
+ finally:
+ self.cleanup()
+
+ # reraise previous interruption now
+ if interrupted:
+ reraise(interrupted[0], interrupted[1], interrupted[2])
+
+ def _print_summary(self, tests):
+ self.logger.info("\nSUMMARY\n-------")
+ self.logger.info("passed: {}".format(self.passed))
+ if self.unexpected_successes == 0:
+ self.logger.info("failed: {}".format(self.failed))
+ else:
+ self.logger.info(
+ "failed: {0} (unexpected sucesses: {1})".format(
+ self.failed, self.unexpected_successes
+ )
+ )
+ if self.skipped == 0:
+ self.logger.info("todo: {}".format(self.todo))
+ else:
+ self.logger.info("todo: {0} (skipped: {1})".format(self.todo, self.skipped))
+
+ if self.failed > 0:
+ self.logger.info("\nFAILED TESTS\n-------")
+ for failed_test in self.failures:
+ self.logger.info("{}".format(failed_test[0]))
+
+ def start_fixture_servers(self):
+ root = self.server_root or os.path.join(os.path.dirname(here), "www")
+ if self.appName == "fennec":
+ return serve.start(root, host=moznetwork.get_ip())
+ else:
+ return serve.start(root)
+
+ def add_test(self, test, expected="pass", group="default"):
+ filepath = os.path.abspath(test)
+
+ if os.path.isdir(filepath):
+ for root, dirs, files in os.walk(filepath):
+ for filename in files:
+ if filename.endswith(".toml"):
+ msg_tmpl = (
+ "Ignoring manifest '{0}'; running all tests in '{1}'."
+ " See --help for details."
+ )
+ relpath = os.path.relpath(
+ os.path.join(root, filename), filepath
+ )
+ self.logger.warning(msg_tmpl.format(relpath, filepath))
+ elif self._is_filename_valid(filename):
+ test_file = os.path.join(root, filename)
+ self.add_test(test_file)
+ return
+
+ file_ext = os.path.splitext(os.path.split(filepath)[-1])[1]
+
+ if file_ext == ".toml":
+ group = filepath
+
+ manifest = TestManifest()
+ manifest.read(filepath)
+
+ json_path = update_mozinfo(filepath)
+ mozinfo.update(
+ {
+ "appname": self.appName,
+ "manage_instance": self.marionette.instance is not None,
+ "headless": self.headless,
+ }
+ )
+ self.logger.info("mozinfo updated from: {}".format(json_path))
+ self.logger.info("mozinfo is: {}".format(mozinfo.info))
+
+ filters = []
+ if self.test_tags:
+ filters.append(tags(self.test_tags))
+
+ manifest_tests = manifest.active_tests(
+ exists=False, disabled=True, filters=filters, **mozinfo.info
+ )
+ if len(manifest_tests) == 0:
+ self.logger.error(
+ "No tests to run using specified "
+ "combination of filters: {}".format(manifest.fmt_filters())
+ )
+
+ target_tests = []
+ for test in manifest_tests:
+ if test.get("disabled"):
+ self.manifest_skipped_tests.append(test)
+ else:
+ target_tests.append(test)
+
+ for i in target_tests:
+ if not os.path.exists(i["path"]):
+ raise IOError("test file: {} does not exist".format(i["path"]))
+
+ self.add_test(i["path"], i["expected"], group=group)
+ return
+
+ self.tests.append({"filepath": filepath, "expected": expected, "group": group})
+
+ def run_test(self, filepath, expected):
+ testloader = unittest.TestLoader()
+ suite = unittest.TestSuite()
+ self.test_kwargs["expected"] = expected
+ mod_name = os.path.splitext(os.path.split(filepath)[-1])[0]
+ for handler in self.test_handlers:
+ if handler.match(os.path.basename(filepath)):
+ handler.add_tests_to_suite(
+ mod_name,
+ filepath,
+ suite,
+ testloader,
+ self.marionette,
+ self.fixtures,
+ self.testvars,
+ **self.test_kwargs
+ )
+ break
+
+ if suite.countTestCases():
+ runner = self.textrunnerclass(
+ logger=self.logger,
+ marionette=self.marionette,
+ capabilities=self.capabilities,
+ result_callbacks=self.result_callbacks,
+ )
+
+ results = runner.run(suite)
+ self.results.append(results)
+
+ self.failed += len(results.failures) + len(results.errors)
+ if hasattr(results, "skipped"):
+ self.skipped += len(results.skipped)
+ self.todo += len(results.skipped)
+ self.passed += results.passed
+ for failure in results.failures + results.errors:
+ self.failures.append(
+ (results.getInfo(failure), failure.output, "TEST-UNEXPECTED-FAIL")
+ )
+ if hasattr(results, "unexpectedSuccesses"):
+ self.failed += len(results.unexpectedSuccesses)
+ self.unexpected_successes += len(results.unexpectedSuccesses)
+ for failure in results.unexpectedSuccesses:
+ self.failures.append(
+ (
+ results.getInfo(failure),
+ failure.output,
+ "TEST-UNEXPECTED-PASS",
+ )
+ )
+ if hasattr(results, "expectedFailures"):
+ self.todo += len(results.expectedFailures)
+
+ self.mixin_run_tests = []
+ for result in self.results:
+ result.result_modifiers = []
+
+ def run_test_set(self, tests):
+ if self.shuffle:
+ random.seed(self.shuffle_seed)
+ random.shuffle(tests)
+
+ for test in tests:
+ self.run_test(test["filepath"], test["expected"])
+ if self.record_crash():
+ break
+
+ def run_test_sets(self):
+ if len(self.tests) < 1:
+ raise Exception("There are no tests to run.")
+ elif self.total_chunks is not None and self.total_chunks > len(self.tests):
+ raise ValueError(
+ "Total number of chunks must be between 1 and {}.".format(
+ len(self.tests)
+ )
+ )
+ if self.total_chunks is not None and self.total_chunks > 1:
+ chunks = [[] for i in range(self.total_chunks)]
+ for i, test in enumerate(self.tests):
+ target_chunk = i % self.total_chunks
+ chunks[target_chunk].append(test)
+
+ self.logger.info(
+ "Running chunk {0} of {1} ({2} tests selected from a "
+ "total of {3})".format(
+ self.this_chunk,
+ self.total_chunks,
+ len(chunks[self.this_chunk - 1]),
+ len(self.tests),
+ )
+ )
+ self.tests = chunks[self.this_chunk - 1]
+
+ self.run_test_set(self.tests)
+
+ def cleanup(self):
+ for proc in serve.iter_proc(self.fixture_servers):
+ proc.stop()
+ proc.kill()
+ self.fixture_servers = {}
+
+ if hasattr(self, "marionette") and self.marionette:
+ if self.marionette.instance is not None:
+ if self.marionette.instance.runner.is_running():
+ # Force a clean shutdown of the application process first if
+ # it is still running. If that fails, kill the process.
+ # Therefore a new session needs to be started.
+ self.marionette.start_session()
+ self.marionette.quit()
+
+ self.marionette.instance.close(clean=True)
+ self.marionette.instance = None
+
+ self.marionette.cleanup()
+ self.marionette = None
+
+ __del__ = cleanup
diff --git a/testing/marionette/harness/marionette_harness/runner/httpd.py b/testing/marionette/harness/marionette_harness/runner/httpd.py
new file mode 100755
index 0000000000..8ffc85aeb0
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/runner/httpd.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python
+
+# 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/.
+
+"""Specialisation of wptserver.server.WebTestHttpd for testing
+Marionette.
+
+"""
+
+import argparse
+import os
+import select
+import sys
+import time
+
+from six.moves.urllib import parse as urlparse
+from wptserve import handlers, request, server
+from wptserve import routes as default_routes
+
+root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
+default_doc_root = os.path.join(root, "www")
+default_ssl_cert = os.path.join(root, "certificates", "test.cert")
+default_ssl_key = os.path.join(root, "certificates", "test.key")
+
+
+@handlers.handler
+def http_auth_handler(req, response):
+ # Allow the test to specify the username and password
+ params = dict(urlparse.parse_qsl(req.url_parts.query))
+ username = params.get("username", "guest")
+ password = params.get("password", "guest")
+
+ auth = request.Authentication(req.headers)
+ content = """<!doctype html>
+<title>HTTP Authentication</title>
+<p id="status">{}</p>"""
+
+ if auth.username == username and auth.password == password:
+ response.status = 200
+ response.content = content.format("success")
+
+ else:
+ response.status = 401
+ response.headers.set("WWW-Authenticate", 'Basic realm="secret"')
+ response.content = content.format("restricted")
+
+
+@handlers.handler
+def upload_handler(request, response):
+ return 200, [], [request.headers.get("Content-Type")] or []
+
+
+@handlers.handler
+def slow_loading_handler(request, response):
+ # Allow the test specify the delay for delivering the content
+ params = dict(urlparse.parse_qsl(request.url_parts.query))
+ delay = int(params.get("delay", 5))
+ time.sleep(delay)
+
+ # Do not allow the page to be cached to circumvent the bfcache of the browser
+ response.headers.set("Cache-Control", "no-cache, no-store")
+ response.content = """<!doctype html>
+<meta charset="UTF-8">
+<title>Slow page loading</title>
+
+<p>Delay: <span id="delay">{}</span></p>
+""".format(
+ delay
+ )
+
+
+@handlers.handler
+def slow_coop_handler(request, response):
+ # Allow the test specify the delay for delivering the content
+ params = dict(urlparse.parse_qsl(request.url_parts.query))
+ delay = int(params.get("delay", 5))
+ time.sleep(delay)
+
+ # Isolate the browsing context exclusively to same-origin documents
+ response.headers.set("Cross-Origin-Opener-Policy", "same-origin")
+ response.headers.set("Cache-Control", "no-cache, no-store")
+ response.content = """<!doctype html>
+<meta charset="UTF-8">
+<title>Slow cross-origin page loading</title>
+
+<p>Delay: <span id="delay">{}</span></p>
+""".format(
+ delay
+ )
+
+
+@handlers.handler
+def update_xml_handler(request, response):
+ response.headers.set("Content-Type", "text/xml")
+ mar_digest = (
+ "75cd68e6c98c84c435cd27e353f5b4f6a3f2c50f6802aa9bf62b47e47138757306769fd9befa08793635ee649"
+ "2319253480860b4aa8ed9ee1caaa4c83ebc90b9"
+ )
+ response.content = """
+ <updates>
+ <update type="minor" displayVersion="9999.0" appVersion="9999.0" platformVersion="9999.0"
+ buildID="20220627075547">
+ <patch type="complete" URL="{}://{}/update/complete.mar" size="86612"
+ hashFunction="sha512" hashValue="{}"/>
+ </update>
+ </updates>
+ """.format(
+ request.url_parts.scheme, request.url_parts.netloc, mar_digest
+ )
+
+
+class NotAliveError(Exception):
+ """Occurs when attempting to run a function that requires the HTTPD
+ to have been started, and it has not.
+
+ """
+
+ pass
+
+
+class FixtureServer(object):
+ def __init__(
+ self,
+ doc_root,
+ url="http://127.0.0.1:0",
+ use_ssl=False,
+ ssl_cert=None,
+ ssl_key=None,
+ ):
+ if not os.path.isdir(doc_root):
+ raise ValueError("Server root is not a directory: %s" % doc_root)
+
+ url = urlparse.urlparse(url)
+ if url.scheme is None:
+ raise ValueError("Server scheme not provided")
+
+ scheme, host, port = url.scheme, url.hostname, url.port
+ if host is None:
+ host = "127.0.0.1"
+ if port is None:
+ port = 0
+
+ routes = [
+ ("POST", "/file_upload", upload_handler),
+ ("GET", "/http_auth", http_auth_handler),
+ ("GET", "/slow", slow_loading_handler),
+ ("GET", "/slow-coop", slow_coop_handler),
+ ("GET", "/update.xml", update_xml_handler),
+ ]
+ routes.extend(default_routes.routes)
+
+ self._httpd = server.WebTestHttpd(
+ host=host,
+ port=port,
+ bind_address=True,
+ doc_root=doc_root,
+ routes=routes,
+ use_ssl=True if scheme == "https" else False,
+ certificate=ssl_cert,
+ key_file=ssl_key,
+ )
+
+ def start(self):
+ if self.is_alive:
+ return
+ self._httpd.start()
+
+ def wait(self):
+ if not self.is_alive:
+ return
+ try:
+ select.select([], [], [])
+ except KeyboardInterrupt:
+ self.stop()
+
+ def stop(self):
+ if not self.is_alive:
+ return
+ self._httpd.stop()
+
+ def get_url(self, path):
+ if not self.is_alive:
+ raise NotAliveError()
+ return self._httpd.get_url(path)
+
+ @property
+ def doc_root(self):
+ return self._httpd.router.doc_root
+
+ @property
+ def router(self):
+ return self._httpd.router
+
+ @property
+ def routes(self):
+ return self._httpd.router.routes
+
+ @property
+ def is_alive(self):
+ return self._httpd.started
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Specialised HTTP server for testing Marionette."
+ )
+ parser.add_argument(
+ "url",
+ help="""
+service address including scheme, hostname, port, and prefix for document root,
+e.g. \"https://0.0.0.0:0/base/\"""",
+ )
+ parser.add_argument(
+ "-r",
+ dest="doc_root",
+ default=default_doc_root,
+ help="path to document root (default %(default)s)",
+ )
+ parser.add_argument(
+ "-c",
+ dest="ssl_cert",
+ default=default_ssl_cert,
+ help="path to SSL certificate (default %(default)s)",
+ )
+ parser.add_argument(
+ "-k",
+ dest="ssl_key",
+ default=default_ssl_key,
+ help="path to SSL certificate key (default %(default)s)",
+ )
+ args = parser.parse_args()
+
+ httpd = FixtureServer(
+ args.doc_root, args.url, ssl_cert=args.ssl_cert, ssl_key=args.ssl_key
+ )
+ httpd.start()
+ print(
+ "{0}: started fixture server on {1}".format(sys.argv[0], httpd.get_url("/")),
+ file=sys.stderr,
+ )
+ httpd.wait()
diff --git a/testing/marionette/harness/marionette_harness/runner/mixins/__init__.py b/testing/marionette/harness/marionette_harness/runner/mixins/__init__.py
new file mode 100644
index 0000000000..71b13461d5
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/runner/mixins/__init__.py
@@ -0,0 +1,5 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from .window_manager import WindowManagerMixin
diff --git a/testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py b/testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py
new file mode 100644
index 0000000000..85729cc585
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py
@@ -0,0 +1,210 @@
+# 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 marionette_driver import Wait
+from six import reraise
+
+
+class WindowManagerMixin(object):
+ def setUp(self):
+ super(WindowManagerMixin, self).setUp()
+
+ self.start_window = self.marionette.current_chrome_window_handle
+ self.start_windows = self.marionette.chrome_window_handles
+
+ self.start_tab = self.marionette.current_window_handle
+ self.start_tabs = self.marionette.window_handles
+
+ def tearDown(self):
+ if len(self.marionette.chrome_window_handles) > len(self.start_windows):
+ raise Exception("Not all windows as opened by the test have been closed")
+
+ if len(self.marionette.window_handles) > len(self.start_tabs):
+ raise Exception("Not all tabs as opened by the test have been closed")
+
+ super(WindowManagerMixin, self).tearDown()
+
+ def close_all_tabs(self):
+ current_window_handles = self.marionette.window_handles
+
+ # If the start tab is not present anymore, use the next one of the list
+ if self.start_tab not in current_window_handles:
+ self.start_tab = current_window_handles[0]
+
+ current_window_handles.remove(self.start_tab)
+ for handle in current_window_handles:
+ self.marionette.switch_to_window(handle)
+ self.marionette.close()
+
+ self.marionette.switch_to_window(self.start_tab)
+
+ def close_all_windows(self):
+ current_chrome_window_handles = self.marionette.chrome_window_handles
+
+ # If the start window is not present anymore, use the next one of the list
+ if self.start_window not in current_chrome_window_handles:
+ self.start_window = current_chrome_window_handles[0]
+ current_chrome_window_handles.remove(self.start_window)
+
+ for handle in current_chrome_window_handles:
+ self.marionette.switch_to_window(handle)
+ self.marionette.close_chrome_window()
+
+ self.marionette.switch_to_window(self.start_window)
+
+ def open_tab(self, callback=None, focus=False):
+ current_tabs = self.marionette.window_handles
+
+ try:
+ if callable(callback):
+ callback()
+ else:
+ result = self.marionette.open(type="tab", focus=focus)
+ if result["type"] != "tab":
+ raise Exception(
+ "Newly opened browsing context is of type {} and not tab.".format(
+ result["type"]
+ )
+ )
+ except Exception:
+ exc_cls, exc, tb = sys.exc_info()
+ reraise(
+ exc_cls,
+ exc_cls("Failed to trigger opening a new tab: {}".format(exc)),
+ tb,
+ )
+ else:
+ Wait(self.marionette).until(
+ lambda mn: len(mn.window_handles) == len(current_tabs) + 1,
+ message="No new tab has been opened",
+ )
+
+ [new_tab] = list(set(self.marionette.window_handles) - set(current_tabs))
+
+ return new_tab
+
+ def open_window(self, callback=None, focus=False, private=False):
+ current_windows = self.marionette.chrome_window_handles
+ current_tabs = self.marionette.window_handles
+
+ def loaded(handle):
+ with self.marionette.using_context("chrome"):
+ return self.marionette.execute_script(
+ """
+ const { windowManager } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/WindowManager.sys.mjs"
+ );
+ const win = windowManager.findWindowByHandle(arguments[0]).win;
+ return win.document.readyState == "complete";
+ """,
+ script_args=[handle],
+ )
+
+ try:
+ if callable(callback):
+ callback(focus)
+ else:
+ result = self.marionette.open(
+ type="window", focus=focus, private=private
+ )
+ if result["type"] != "window":
+ raise Exception(
+ "Newly opened browsing context is of type {} and not window.".format(
+ result["type"]
+ )
+ )
+ except Exception:
+ exc_cls, exc, tb = sys.exc_info()
+ reraise(
+ exc_cls,
+ exc_cls("Failed to trigger opening a new window: {}".format(exc)),
+ tb,
+ )
+ else:
+ Wait(self.marionette).until(
+ lambda mn: len(mn.chrome_window_handles) == len(current_windows) + 1,
+ message="No new window has been opened",
+ )
+
+ [new_window] = list(
+ set(self.marionette.chrome_window_handles) - set(current_windows)
+ )
+
+ # Before continuing ensure the window has been completed loading
+ Wait(self.marionette).until(
+ lambda _: loaded(new_window),
+ message="Window with handle '{}'' did not finish loading".format(
+ new_window
+ ),
+ )
+
+ # Bug 1507771 - Return the correct handle based on the currently selected context
+ # as long as "WebDriver:NewWindow" is not handled separtely in chrome context
+ context = self.marionette._send_message(
+ "Marionette:GetContext", key="value"
+ )
+ if context == "chrome":
+ return new_window
+ elif context == "content":
+ [new_tab] = list(
+ set(self.marionette.window_handles) - set(current_tabs)
+ )
+ return new_tab
+
+ def open_chrome_window(self, url, focus=False):
+ """Open a new chrome window with the specified chrome URL.
+
+ Can be replaced with "WebDriver:NewWindow" once the command
+ supports opening generic chrome windows beside browsers (bug 1507771).
+ """
+
+ def open_with_js(focus):
+ with self.marionette.using_context("chrome"):
+ self.marionette.execute_async_script(
+ """
+ let [url, focus, resolve] = arguments;
+
+ function waitForEvent(target, type, args) {
+ return new Promise(resolve => {
+ let params = Object.assign({once: true}, args);
+ target.addEventListener(type, event => {
+ dump(`** Received DOM event ${event.type} for ${event.target}\n`);
+ resolve();
+ }, params);
+ });
+ }
+
+ function waitForFocus(win) {
+ return Promise.all([
+ waitForEvent(win, "activate"),
+ waitForEvent(win, "focus", {capture: true}),
+ ]);
+ }
+
+ (async function() {
+ // Open a window, wait for it to receive focus
+ let win = window.openDialog(url, null, "chrome,centerscreen");
+ let focused = waitForFocus(win);
+
+ win.focus();
+ await focused;
+
+ // The new window shouldn't get focused. As such set the
+ // focus back to the opening window.
+ if (!focus && Services.focus.activeWindow != window) {
+ let focused = waitForFocus(window);
+ window.focus();
+ await focused;
+ }
+
+ resolve(win.docShell.browsingContext.id);
+ })();
+ """,
+ script_args=(url, focus),
+ )
+
+ with self.marionette.using_context("chrome"):
+ return self.open_window(callback=open_with_js, focus=focus)
diff --git a/testing/marionette/harness/marionette_harness/runner/serve.py b/testing/marionette/harness/marionette_harness/runner/serve.py
new file mode 100755
index 0000000000..3833bbe876
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/runner/serve.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python
+
+# 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/.
+
+"""Spawns necessary HTTP servers for testing Marionette in child
+processes.
+
+"""
+
+import argparse
+import multiprocessing
+import os
+import sys
+from collections import defaultdict
+
+from six import iteritems
+
+from . import httpd
+
+__all__ = [
+ "default_doc_root",
+ "iter_proc",
+ "iter_url",
+ "registered_servers",
+ "servers",
+ "start",
+ "where_is",
+]
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+class BlockingChannel(object):
+ def __init__(self, channel):
+ self.chan = channel
+ self.lock = multiprocessing.Lock()
+
+ def call(self, func, args=()):
+ self.send((func, args))
+ return self.recv()
+
+ def send(self, *args):
+ try:
+ self.lock.acquire()
+ self.chan.send(args)
+ finally:
+ self.lock.release()
+
+ def recv(self):
+ try:
+ self.lock.acquire()
+ payload = self.chan.recv()
+ if isinstance(payload, tuple) and len(payload) == 1:
+ return payload[0]
+ return payload
+ except KeyboardInterrupt:
+ return ("stop", ())
+ finally:
+ self.lock.release()
+
+
+class ServerProxy(multiprocessing.Process, BlockingChannel):
+ def __init__(self, channel, init_func, *init_args, **init_kwargs):
+ multiprocessing.Process.__init__(self)
+ BlockingChannel.__init__(self, channel)
+ self.init_func = init_func
+ self.init_args = init_args
+ self.init_kwargs = init_kwargs
+
+ def run(self):
+ try:
+ server = self.init_func(*self.init_args, **self.init_kwargs)
+ server.start()
+ self.send(("ok", ()))
+
+ while True:
+ # ["func", ("arg", ...)]
+ # ["prop", ()]
+ sattr, fargs = self.recv()
+ attr = getattr(server, sattr)
+
+ # apply fargs to attr if it is a function
+ if callable(attr):
+ rv = attr(*fargs)
+
+ # otherwise attr is a property
+ else:
+ rv = attr
+
+ self.send(rv)
+
+ if sattr == "stop":
+ return
+
+ except Exception as e:
+ self.send(("stop", e))
+
+ except KeyboardInterrupt:
+ server.stop()
+
+
+class ServerProc(BlockingChannel):
+ def __init__(self, init_func):
+ self._init_func = init_func
+ self.proc = None
+
+ parent_chan, self.child_chan = multiprocessing.Pipe()
+ BlockingChannel.__init__(self, parent_chan)
+
+ def start(self, doc_root, ssl_config, **kwargs):
+ self.proc = ServerProxy(
+ self.child_chan, self._init_func, doc_root, ssl_config, **kwargs
+ )
+ self.proc.daemon = True
+ self.proc.start()
+
+ res, exc = self.recv()
+ if res == "stop":
+ raise exc
+
+ def get_url(self, url):
+ return self.call("get_url", (url,))
+
+ @property
+ def doc_root(self):
+ return self.call("doc_root", ())
+
+ def stop(self):
+ self.call("stop")
+ if not self.is_alive:
+ return
+ self.proc.join()
+
+ def kill(self):
+ if not self.is_alive:
+ return
+ self.proc.terminate()
+ self.proc.join(0)
+
+ @property
+ def is_alive(self):
+ if self.proc is not None:
+ return self.proc.is_alive()
+ return False
+
+
+def http_server(doc_root, ssl_config, host="127.0.0.1", **kwargs):
+ return httpd.FixtureServer(doc_root, url="http://{}:0/".format(host), **kwargs)
+
+
+def https_server(doc_root, ssl_config, host="127.0.0.1", **kwargs):
+ return httpd.FixtureServer(
+ doc_root,
+ url="https://{}:0/".format(host),
+ ssl_key=ssl_config["key_path"],
+ ssl_cert=ssl_config["cert_path"],
+ **kwargs
+ )
+
+
+def start_servers(doc_root, ssl_config, **kwargs):
+ servers = defaultdict()
+ for schema, builder_fn in registered_servers:
+ proc = ServerProc(builder_fn)
+ proc.start(doc_root, ssl_config, **kwargs)
+ servers[schema] = (proc.get_url("/"), proc)
+ return servers
+
+
+def start(doc_root=None, **kwargs):
+ """Start all relevant test servers.
+
+ If no `doc_root` is given the default
+ testing/marionette/harness/marionette_harness/www directory will be used.
+
+ Additional keyword arguments can be given which will be passed on
+ to the individual ``FixtureServer``'s in httpd.py.
+
+ """
+ doc_root = doc_root or default_doc_root
+ ssl_config = {
+ "cert_path": httpd.default_ssl_cert,
+ "key_path": httpd.default_ssl_key,
+ }
+
+ global servers
+ servers = start_servers(doc_root, ssl_config, **kwargs)
+ return servers
+
+
+def where_is(uri, on="http"):
+ """Returns the full URL, including scheme, hostname, and port, for
+ a fixture resource from the server associated with the ``on`` key.
+ It will by default look for the resource in the "http" server.
+
+ """
+ return servers.get(on)[1].get_url(uri)
+
+
+def iter_proc(servers):
+ for _, (_, proc) in iteritems(servers):
+ yield proc
+
+
+def iter_url(servers):
+ for _, (url, _) in iteritems(servers):
+ yield url
+
+
+default_doc_root = os.path.join(os.path.dirname(here), "www")
+registered_servers = [("http", http_server), ("https", https_server)]
+servers = defaultdict()
+
+
+def main(args):
+ global servers
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-r", dest="doc_root", help="Path to document root. Overrides default."
+ )
+ args = parser.parse_args()
+
+ servers = start(args.doc_root)
+ for url in iter_url(servers):
+ print("{}: listening on {}".format(sys.argv[0], url), file=sys.stderr)
+
+ try:
+ while any(proc.is_alive for proc in iter_proc(servers)):
+ for proc in iter_proc(servers):
+ proc.proc.join(1)
+ except KeyboardInterrupt:
+ for proc in iter_proc(servers):
+ proc.kill()
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/testing/marionette/harness/marionette_harness/runtests.py b/testing/marionette/harness/marionette_harness/runtests.py
new file mode 100644
index 0000000000..0d86e1534d
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/runtests.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 sys
+
+import mozlog
+from marionette_driver import __version__ as driver_version
+
+from marionette_harness import (
+ BaseMarionetteArguments,
+ BaseMarionetteTestRunner,
+ MarionetteTestCase,
+ __version__,
+)
+
+
+class MarionetteTestRunner(BaseMarionetteTestRunner):
+ def __init__(self, **kwargs):
+ BaseMarionetteTestRunner.__init__(self, **kwargs)
+ self.test_handlers = [MarionetteTestCase]
+
+
+class MarionetteArguments(BaseMarionetteArguments):
+ pass
+
+
+class MarionetteHarness(object):
+ def __init__(
+ self,
+ runner_class=MarionetteTestRunner,
+ parser_class=MarionetteArguments,
+ testcase_class=MarionetteTestCase,
+ args=None,
+ ):
+ self._runner_class = runner_class
+ self._parser_class = parser_class
+ self._testcase_class = testcase_class
+ self.args = args or self.parse_args()
+
+ def parse_args(self, logger_defaults=None):
+ parser = self._parser_class(
+ usage="%(prog)s [options] test_file_or_dir <test_file_or_dir> ..."
+ )
+ parser.add_argument(
+ "--version",
+ action="version",
+ help="Show version information.",
+ version="%(prog)s {version}"
+ " (using marionette-driver: {driver_version}, ".format(
+ version=__version__, driver_version=driver_version
+ ),
+ )
+ mozlog.commandline.add_logging_group(parser)
+ args = parser.parse_args()
+ parser.verify_usage(args)
+
+ logger = mozlog.commandline.setup_logging(
+ args.logger_name, args, logger_defaults or {"tbpl": sys.stdout}
+ )
+
+ args.logger = logger
+ return vars(args)
+
+ def process_args(self):
+ if self.args.get("pydebugger"):
+ self._testcase_class.pydebugger = __import__(self.args["pydebugger"])
+ # Remove mozlog arguments from the return value since these aren't
+ # used directly by the rest of marionette
+ self.args = {
+ key: value for key, value in self.args.items() if not key.startswith("log_")
+ }
+
+ def run(self):
+ self.process_args()
+ tests = self.args.pop("tests")
+ runner = self._runner_class(**self.args)
+ try:
+ runner.run_tests(tests)
+ finally:
+ runner.cleanup()
+ return runner.failed + runner.crashed
+
+
+def cli(
+ runner_class=MarionetteTestRunner,
+ parser_class=MarionetteArguments,
+ harness_class=MarionetteHarness,
+ testcase_class=MarionetteTestCase,
+ args=None,
+):
+ """
+ Call the harness to parse args and run tests.
+
+ The following exit codes are expected:
+ - Test failures: 10
+ - Harness/other failures: 1
+ - Success: 0
+ """
+ logger = mozlog.commandline.setup_logging("Marionette test runner", {})
+ try:
+ harness_instance = harness_class(
+ runner_class, parser_class, testcase_class, args=args
+ )
+ failed = harness_instance.run()
+ if failed > 0:
+ sys.exit(10)
+ except Exception as e:
+ logger.error(str(e), exc_info=True)
+ sys.exit(1)
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ cli()
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
diff --git a/testing/marionette/harness/marionette_harness/www/actions_scroll.html b/testing/marionette/harness/marionette_harness/www/actions_scroll.html
new file mode 100644
index 0000000000..468a699696
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/actions_scroll.html
@@ -0,0 +1,139 @@
+<!doctype html>
+<meta charset=utf-8>
+<html>
+ <head>
+ <title>Test Scroll Actions</title>
+ <style>
+ div {
+ padding: 0;
+ margin: 0;
+ }
+
+ #not-scrollable {
+ margin-bottom: 100px;
+ width: 100px;
+ height: 50px;
+ }
+
+ #not-scrollable-content {
+ width: 200px;
+ height: 100px;
+ background-color: #ccc;
+ }
+
+ #scrollable {
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+ }
+
+ #scrollable-content {
+ width: 600px;
+ height: 1000px;
+ background-color: blue;
+ }
+
+ #iframe {
+ width: 100px;
+ height: 100px;
+ }
+
+ #event-reporter {
+ white-space: pre-line;
+ }
+ </style>
+
+ <script>
+ var eventReporter;
+ var allEvents = { events: [] };
+
+ function addMessage(message) {
+ eventReporter.textContent = `${message}\n${eventReporter.textContent}`;
+ }
+
+ function recordWheelEvent(event) {
+ allEvents.events.push({
+ "type": event.type,
+ "button": event.button,
+ "buttons": event.buttons,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "deltaX": event.deltaX,
+ "deltaY": event.deltaY,
+ "deltaZ": event.deltaZ,
+ "deltaMode": event.deltaMode,
+ "target": event.target.id,
+ });
+
+ addMessage(
+ "type: " + event.type + " " +
+ "button: " + event.button + ", " +
+ "buttons: " + event.buttons + ", " +
+ "pageX: " + event.pageX + ", " +
+ "pageY: " + event.pageY + ", " +
+ "deltaX: " + event.deltaX + ", " +
+ "deltaY: " + event.deltaY + ", " +
+ "deltaZ: " + event.deltaZ + ", " +
+ "deltaMode: " + event.deltaMode + ", " +
+ "target id: " + event.target.id
+ );
+ }
+
+ document.addEventListener("DOMContentLoaded", function () {
+ eventReporter = document.getElementById("event-reporter");
+
+ var noScroll = document.getElementById("not-scrollable");
+ noScroll.addEventListener("wheel", recordWheelEvent);
+
+ var scrollable = document.getElementById("scrollable");
+ scrollable.addEventListener("wheel", recordWheelEvent);
+ });
+ </script>
+ </head>
+
+ <body>
+ <div>
+ <h2>Scroll Reporter</h2>
+ <div id="not-scrollable">
+ <div id="not-scrollable-content"></div>
+ </div>
+ </div>
+
+ <div>
+ <h2>Overflow Scroll Reporter</h2>
+ <div id="scrollable">
+ <div id="scrollable-content"></div>
+ </div>
+ </div>
+
+ <div>
+ <h2>iframe Scroll Reporter</h2>
+ <iframe id="iframe" srcdoc='
+ <script>
+ document.scrollingElement.addEventListener("wheel", event => {
+ window.parent.recordWheelEvent({
+ "type": event.type,
+ "button": event.button,
+ "buttons": event.buttons,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "deltaX": event.deltaX,
+ "deltaY": event.deltaY,
+ "deltaZ": event.deltaZ,
+ "deltaMode": event.deltaMode,
+ "target": event.target
+ });
+ });
+ </script>
+ <div id="iframeContent" style="width: 7500px; height: 7500px; background-color:blue">
+ </div>'>
+ </iframe>
+ </div>
+
+ <div id="resultContainer">
+ <hr />
+ <h2>Events</h2>
+ <div id="event-reporter"></div>
+ </div>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/addons/webextension-signed.xpi b/testing/marionette/harness/marionette_harness/www/addons/webextension-signed.xpi
new file mode 100644
index 0000000000..5363911af1
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/addons/webextension-signed.xpi
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/addons/webextension-unsigned.xpi b/testing/marionette/harness/marionette_harness/www/addons/webextension-unsigned.xpi
new file mode 100644
index 0000000000..cf0fad63b5
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/addons/webextension-unsigned.xpi
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/black.png b/testing/marionette/harness/marionette_harness/www/black.png
new file mode 100644
index 0000000000..b62a3a7bc8
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/black.png
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/bug814037.html b/testing/marionette/harness/marionette_harness/www/bug814037.html
new file mode 100644
index 0000000000..47c2968163
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/bug814037.html
@@ -0,0 +1,56 @@
+<html>
+<head>
+<meta name="viewport" content="minimum-scale=1,width=device-width">
+<style>
+body {
+ width: 100%;
+ margin: 0px;
+ transition: transform 300ms ease;
+ overflow-x: hidden;
+}
+
+body.section1 {
+ transform: translateX(0%);
+}
+
+body.section2 {
+ transform: translateX(-100%);
+}
+
+section {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+}
+
+#section1 {
+ left: 0px;
+}
+
+#section2 {
+ left: 100%;
+}
+.mypossie {
+ position:absolute;
+ left: -1000px;
+}
+</style>
+
+</head>
+ <body class="section1">
+ <section id="section1">
+ <div id="assertMe1">
+ <p>Section 1</p>
+ </div>
+ <button id="b1" onclick="var sect = document.getElementsByTagName('body')[0]; sect.classList.add('section2'); sect.classList.remove('section1');">Show section 2</button>
+ </section>
+
+ <section id="section2">
+ <div id="assertMe2">
+ <p>Section 2</p>
+ </div>
+ <button id="b2" onclick="var sect = document.getElementsByTagName('body')[0]; sect.classList.add('section1'); sect.classList.remove('section2'); ">Show section 1</button>
+ </section>
+ <section class='mypossie'>out in left field!</section>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/click_out_of_bounds_overflow.html b/testing/marionette/harness/marionette_harness/www/click_out_of_bounds_overflow.html
new file mode 100644
index 0000000000..f0bee9b469
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/click_out_of_bounds_overflow.html
@@ -0,0 +1,90 @@
+<!-- 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>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<body>
+<div style="height: 100px; overflow: auto;">
+ <table>
+ <tbody>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td>data</td></tr>
+ <tr><td><a href="#clicked" id="link">click me</a></td></tr>
+ </tbody>
+ </table>
+</div>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/clicks.html b/testing/marionette/harness/marionette_harness/www/clicks.html
new file mode 100644
index 0000000000..96e9f55171
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/clicks.html
@@ -0,0 +1,57 @@
+<html>
+<head>
+ <!-- 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/. -->
+ <title>Testing Clicks</title>
+
+ <script>
+ function addMousedownListener() {
+ let el = document.getElementById('showbutton');
+
+ el.addEventListener('mousedown', function (evt) {
+ evt.target.innerText = evt.button;
+ });
+ }
+ </script>
+</head>
+
+<body>
+<h1>Testing Clicks</h1>
+
+<div>
+ <p id="links">Links:</p>
+ <ul>
+ <li><a href="test.html">333333</a>
+ <li><a href="test.html" id="normal">Normal</a>
+ <li><a href="#" id="anchor">I go to an anchor</a>
+ <li><a href="addons/webextension-unsigned.xpi" id="install-addon">Install Add-on</a>
+ </ul>
+</div>
+
+<div>
+ <p id="js-links">Javascript links:</p>
+ <ul>
+ <li>Navigate in history:
+ <a href="javascript:history.back();" id="history-back">Back</a>
+ <a href="javascript:history.forward();" id="history-forward">Forward</a>
+ <li><a href="javascript:window.open('test.html', '_blank')" id="new-window">Open a window</a>
+ <li><a href="javascript:window.close();" id="close-window">Close tab/window</a>
+ <li><a id="addbuttonlistener" href="javascript:addMousedownListener();">Click</a> to
+ add an event listener for: <span style="color: red;" id="showbutton">button click</span>
+ </ul>
+</div>
+
+<div>
+ <p id="special">Special:</p>
+ <select id="option" onclick="window.location = '/slow?delay=1'">
+ <option>Click to navigate</option>
+ </select>
+
+ <p style="background-color: rgb(0, 255, 0); width: 5em;">
+ <a id="overflowLink" href="test.html">looooooooooong short looooooooooong</a>
+ </p>
+</div>
+
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/dom/cache/basicCacheAPI_PBM.html b/testing/marionette/harness/marionette_harness/www/dom/cache/basicCacheAPI_PBM.html
new file mode 100644
index 0000000000..8a23acf437
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/dom/cache/basicCacheAPI_PBM.html
@@ -0,0 +1,21 @@
+<html>
+ <head>
+ <script>
+ async function ensureCache(name) {
+ if (!window.testCache) {
+ window.testCache = await caches.open(name);
+ }
+ return window.testCache;
+ };
+
+ function releaseCache() {
+ window.testCache = null;
+ }
+
+ async function addDataIntoCache(name, request, response) {
+ let cache = await ensureCache(name);
+ return cache.put(request, response);
+ };
+ </script>
+ </head>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/dom/cache/cacheUsage.html b/testing/marionette/harness/marionette_harness/www/dom/cache/cacheUsage.html
new file mode 100644
index 0000000000..8bb7da8f71
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/dom/cache/cacheUsage.html
@@ -0,0 +1,28 @@
+<html>
+ <head>
+ <script>
+ async function getStorageEstimate() {
+ let r = await navigator.storage.estimate();
+ return r.usage;
+ }
+
+ function openCache(id) {
+ return caches.open(id);
+ }
+
+ async function doCacheWork(id, n) {
+ let c = await openCache(id);
+
+ const body = new Uint32Array(1024);
+ self.crypto.getRandomValues(body);
+
+ for (let i = 0; i < n; i++) {
+ await c.put(new Request(`/data-${i}`), new Response(body))
+ }
+
+ await caches.delete(id)
+ return "success";
+ }
+ </script>
+ </head>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/dom/indexedDB/basicIDB_PBM.html b/testing/marionette/harness/marionette_harness/www/dom/indexedDB/basicIDB_PBM.html
new file mode 100644
index 0000000000..90472d64d2
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/dom/indexedDB/basicIDB_PBM.html
@@ -0,0 +1,49 @@
+<html>
+ <head>
+ <script>
+ async function ensureIDB(name, ver, store) {
+ return new Promise((resolve, reject) => {
+ let createObjectStore = (db, store) => {
+ db.createObjectStore(store);
+ };
+
+ var req = indexedDB.open(name, ver);
+ req.onerror = reject;
+
+ req.onsuccess = (event) => {
+ resolve(req.result);
+ };
+
+ req.onupgradeneeded = function (event) {
+ let db = event.target.result;
+ createObjectStore(db, store);
+ };
+ });
+ };
+
+ async function addDataIntoIDB(idb, store, key, value) {
+ let db = await ensureIDB(idb, 1, store);
+ await (new Promise((resolve, reject) => {
+ var transaction = db.transaction([store], "readwrite");
+ var put = transaction.objectStore(store).put(value, key);
+ put.onerror = reject;
+ put.onsuccess = resolve;
+ }));
+
+ closeIDB(db)
+ };
+
+ function closeIDB(db) {
+ db.close();
+ }
+
+ function deleteIDB(db) {
+ return new Promise((resolve, reject) => {
+ let deleteReq = indexedDB.deleteDatabase(db);
+ deleteReq.onerror = reject;
+ deleteReq.onsuccess = resolve;
+ });
+ }
+ </script>
+ </head>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/element_outside_viewport.html b/testing/marionette/harness/marionette_harness/www/element_outside_viewport.html
new file mode 100644
index 0000000000..69b66b8759
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/element_outside_viewport.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<style>
+ div {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ }
+ .top { background-color: red; }
+ #top-70 { left: 80px; top: 0; }
+ #top-50 { left: 190px; top: 20px; }
+ #top-30 { left: 300px; top: 40px; }
+
+ .right { background-color: black; }
+ #right-70 { top: 80px; right: -140px;}
+ #right-50 { top: 190px; right: -120px;}
+ #right-30 { top: 300px; right: -100px;}
+
+ .bottom { background-color: blue; }
+ #bottom-70 { right: -50px; bottom: -140px; }
+ #bottom-50 { right: 60px; bottom: -120px; }
+ #bottom-30 { right: 170px; bottom: -100px; }
+
+ .left { background-color: green; }
+ #left-70 { bottom: -50px; left: 0; }
+ #left-50 { bottom: 60px; left: 20px; }
+ #left-30 { bottom: 170px; left: 40px; }
+</style>
+<body onload="window.scrollTo(70, 70);">
+ <div id="top-70" class="top"></div>
+ <div id="top-50" class="top"></div>
+ <div id="top-30" class="top"></div>
+ <div id="right-70" class="right"></div>
+ <div id="right-50" class="right"></div>
+ <div id="right-30" class="right"></div>
+ <div id="bottom-70" class="bottom"></div>
+ <div id="bottom-50" class="bottom"></div>
+ <div id="bottom-30" class="bottom"></div>
+ <div id="left-70" class="left"></div>
+ <div id="left-50" class="left"></div>
+ <div id="left-30" class="left"></div>
+</body>
diff --git a/testing/marionette/harness/marionette_harness/www/empty.html b/testing/marionette/harness/marionette_harness/www/empty.html
new file mode 100644
index 0000000000..646edf9a72
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/empty.html
@@ -0,0 +1,12 @@
+<!-- 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>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/formPage.html b/testing/marionette/harness/marionette_harness/www/formPage.html
new file mode 100644
index 0000000000..43fde32431
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/formPage.html
@@ -0,0 +1,114 @@
+<html>
+<head>
+ <title>We Leave From Here</title>
+
+ <script type="text/javascript">
+ function changePage() {
+ let newLocation = '/common/page/3';
+ window.location = newLocation;
+ }
+ </script>
+</head>
+<body>
+There should be a form here:
+
+<form method="get" action="resultPage.html" name="login">
+ <input type="email" id="email"/>
+ <input type="submit" id="submitButton" value="Hello there"/>
+</form>
+
+<form method="get" action="resultPage.html" name="optional" style="display: block">
+ Here's a checkbox:
+ <input type="checkbox" id="checky" name="checky" value="furrfu"/>
+ <input type="checkbox" id="checkedchecky" name="checkedchecky" checked="checked" />
+ <input type="checkbox" id="disabledchecky" disabled="disabled" name="disabledchecky" />
+ <input type="checkbox" id="randomly_disabled_checky" disabled="somerandomstring" checked="checked" name="randomlydisabledchecky" />
+ <br/>
+ <select name="selectomatic">
+ <option selected="selected" id="non_multi_option" value="one">One</option>
+ <option value="two">Two</option>
+ <option value="four">Four</option>
+ <option value="still learning how to count, apparently">Still learning how to count, apparently</option>
+ </select>
+
+ <select name="multi" id="multi" multiple="multiple">
+ <option selected="selected" value="eggs">Eggs</option>
+ <option value="ham">Ham</option>
+ <option selected="selected" value="sausages">Sausages</option>
+ <option value="onion gravy">Onion gravy</option>
+ </select>
+
+ <select name="no-select" disabled="disabled">
+ <option value="foo">Foo</option>
+ </select>
+
+ <select name="select_empty_multiple" multiple>
+ <option id="multi_1" value="select_1">select_1</option>
+ <option id="multi_2" value="select_2">select_2</option>
+ <option id="multi_3" value="select_3">select_3</option>
+ <option id="multi_4" value="select_4">select_4</option>
+ </select>
+
+ <select name="multi_true" multiple="true">
+ <option id="multi_true_1" value="select_1">select_1</option>
+ <option id="multi_true_2" value="select_2">select_2</option>
+ </select>
+
+ <select name="multi_false" multiple="false">
+ <option id="multi_false_1" value="select_1">select_1</option>
+ <option id="multi_false_2" value="select_2">select_2</option>
+ </select>
+
+ <select id="invisi_select" style="opacity:0;">
+ <option selected value="apples">Apples</option>
+ <option value="oranges">Oranges</option>
+ </select>
+
+ <select name="select-default">
+ <option>One</option>
+ <option>Two</option>
+ <option>Four</option>
+ <option>Still learning how to count, apparently</option>
+ </select>
+
+ <select name="select_with_spaces">
+ <option>One</option>
+ <option> Two </option>
+ <option>
+ Four
+ </option>
+ <option>
+ Still learning how to count,
+ apparently
+ </option>
+ </select>
+
+ <select>
+ <option id="blankOption"></option>
+ <option id="optionEmptyValueSet" value="">nothing</option>
+ </select>
+
+ <br/>
+
+ <input type="radio" id="cheese" name="snack" value="cheese"/>Cheese<br/>
+ <input type="radio" id="peas" name="snack" value="peas"/>Peas<br/>
+ <input type="radio" id="cheese_and_peas" name="snack" value="cheese and peas" checked/>Cheese and peas<br/>
+ <input type="radio" id="nothing" name="snack" value="nowt" disabled="disabled"/>Not a sausage<br/>
+ <input type="radio" id="randomly_disabled_nothing" name="snack" value="funny nowt" disabled="somedisablingstring"/>Not another sausage
+
+ <input type="hidden" name="hidden" value="fromage" />
+
+ <p id="cheeseLiker">I like cheese</p>
+ <input type="submit" value="Click!"/>
+
+ <input type="radio" id="lone_disabled_selected_radio" name="not_a_snack" value="cumberland" checked="checked" disabled="disabled" />Cumberland sausage
+</form>
+
+<form method="get" action="formPage.html">
+ <p>
+ <label for="checkbox-with-label" id="label-for-checkbox-with-label">Label</label><input type="checkbox" id="checkbox-with-label" />
+ </p>
+</form>
+<input id="vsearchGadget" name="SearchableText" type="text" size="18" value="" title="Hvad søger du?" accesskey="4" class="inputLabel" />
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/frameset.html b/testing/marionette/harness/marionette_harness/www/frameset.html
new file mode 100644
index 0000000000..e91472c952
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/frameset.html
@@ -0,0 +1,13 @@
+<html>
+ <head>
+ <title>Unique title</title>
+ </head>
+<frameset cols="*, *, *, *, *, *, *">
+ <frame name="first" src="page/1"/>
+ <frame name="second" src="page/2?title=Fish"/>
+ <frame name="third" src="formPage.html"/>
+ <frame name="fourth" src="framesetPage2.html"/>
+ <frame id="fifth" src="xhtmlTest.html"/>
+ <frame id="sixth" src="test_iframe.html"/>
+ <frame id="sixth.iframe1" src="page/3"/>
+</frameset>
diff --git a/testing/marionette/harness/marionette_harness/www/framesetPage2.html b/testing/marionette/harness/marionette_harness/www/framesetPage2.html
new file mode 100644
index 0000000000..5190ceb6ce
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/framesetPage2.html
@@ -0,0 +1,7 @@
+<html>
+<head></head>
+<frameset cols="*, *">
+ <frame name="child1" src="test.html"/>
+ <frame name="child2" src="test.html"/>
+</frameset>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/html5/blue.jpg b/testing/marionette/harness/marionette_harness/www/html5/blue.jpg
new file mode 100644
index 0000000000..8ea27c42fa
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/blue.jpg
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/html5/boolean_attributes.html b/testing/marionette/harness/marionette_harness/www/html5/boolean_attributes.html
new file mode 100644
index 0000000000..431e575aef
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/boolean_attributes.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<input id='disabled' disabled>
diff --git a/testing/marionette/harness/marionette_harness/www/html5/geolocation.js b/testing/marionette/harness/marionette_harness/www/html5/geolocation.js
new file mode 100644
index 0000000000..4fb4a4747b
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/geolocation.js
@@ -0,0 +1,29 @@
+/* eslint-disable no-unsanitized/property */
+
+function success(position) {
+ let message = document.getElementById("status");
+ message.innerHTML =
+ "<img src='http://maps.google.com/maps/api/staticmap?center=" +
+ position.coords.latitude +
+ "," +
+ position.coords.longitude +
+ "&size=300x200&maptype=roadmap&zoom=12&&markers=size:mid|color:red|" +
+ position.coords.latitude +
+ "," +
+ position.coords.longitude +
+ "&sensor=false' />";
+ message.innerHTML += "<p>Longitude: " + position.coords.longitude + "</p>";
+ message.innerHTML += "<p>Latitude: " + position.coords.latitude + "</p>";
+ message.innerHTML += "<p>Altitude: " + position.coords.altitude + "</p>";
+}
+
+function error(msg) {
+ let message = document.getElementById("status");
+ message.innerHTML = "Failed to get geolocation.";
+}
+
+if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(success, error);
+} else {
+ error("Geolocation is not supported.");
+}
diff --git a/testing/marionette/harness/marionette_harness/www/html5/green.jpg b/testing/marionette/harness/marionette_harness/www/html5/green.jpg
new file mode 100644
index 0000000000..6a0d3bea47
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/green.jpg
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/html5/offline.html b/testing/marionette/harness/marionette_harness/www/html5/offline.html
new file mode 100644
index 0000000000..c24178b5f5
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/offline.html
@@ -0,0 +1 @@
+<html><head><title>Offline</title></head><body></body></html>
diff --git a/testing/marionette/harness/marionette_harness/www/html5/red.jpg b/testing/marionette/harness/marionette_harness/www/html5/red.jpg
new file mode 100644
index 0000000000..f296e27195
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/red.jpg
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/html5/status.html b/testing/marionette/harness/marionette_harness/www/html5/status.html
new file mode 100644
index 0000000000..394116a522
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/status.html
@@ -0,0 +1 @@
+<html><head><title>Online</title></head><body></body></html>
diff --git a/testing/marionette/harness/marionette_harness/www/html5/test.appcache b/testing/marionette/harness/marionette_harness/www/html5/test.appcache
new file mode 100644
index 0000000000..3bc4e00257
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/test.appcache
@@ -0,0 +1,11 @@
+CACHE MANIFEST
+
+CACHE:
+# Additional items to cache.
+yellow.jpg
+red.jpg
+blue.jpg
+green.jpg
+
+FALLBACK:
+status.html offline.html
diff --git a/testing/marionette/harness/marionette_harness/www/html5/test_html_inputs.html b/testing/marionette/harness/marionette_harness/www/html5/test_html_inputs.html
new file mode 100644
index 0000000000..a170ced1ab
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/test_html_inputs.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<input id='number' type=number>
diff --git a/testing/marionette/harness/marionette_harness/www/html5/yellow.jpg b/testing/marionette/harness/marionette_harness/www/html5/yellow.jpg
new file mode 100644
index 0000000000..7c609b3712
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5/yellow.jpg
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/html5Page.html b/testing/marionette/harness/marionette_harness/www/html5Page.html
new file mode 100644
index 0000000000..fbd943d792
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/html5Page.html
@@ -0,0 +1,46 @@
+<html manifest="html5/test.appcache">
+<!--
+Copyright 2011 Software Freedom Conservancy.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+
+<head>
+<title>HTML5</title>
+</head>
+<body>
+
+<h3>Geolocation Test</h3>
+<div id="status">Location unknown</div>
+<script language="javascript" type="text/javascript" src="html5/geolocation.js"></script>
+
+<h3>Application Cache Test</h3>
+<div id="images">
+ <p>Current network status: <span id="state"></span></p>
+ <script>
+ const state = document.getElementById('state')
+ setInterval(function () {
+ state.className = navigator.onLine ? 'online' : 'offline';
+ // eslint-disable-next-line no-unsanitized/property
+ state.innerHTML = navigator.onLine ? 'online' : 'offline';
+ }, 250);
+ </script>
+ <img id="red" src="html5/red.jpg">
+ <img id="blue" src="html5/blue.jpg">
+ <img id="green" src="html5/green.jpg">
+ <img id="yellow" src="html5/yellow.jpg">
+</div>
+
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/keyboard.html b/testing/marionette/harness/marionette_harness/www/keyboard.html
new file mode 100644
index 0000000000..e711b31e05
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/keyboard.html
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <!-- 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/. -->
+
+<head>
+ <title>Testing Javascript</title>
+ <meta name="viewport" content="user-scalable=no">
+ <script type="text/javascript">
+ const seen = {};
+
+ function updateResult(event) {
+ document.getElementById('result').innerText = event.target.value;
+ }
+
+ function displayMessage(message) {
+ document.getElementById('result').innerText = message;
+ }
+
+ function appendMessage(message) {
+ document.getElementById('result').innerText += " " + message + " ";
+ }
+ </script>
+</head>
+
+<body>
+<h1>Type Stuff</h1>
+
+<div id="resultContainer">
+ Result: <p id="result"></p>
+</div>
+
+<div>
+ <form action="#">
+ <p>
+ <label>keyDown: <input type="text" id="keyDown" onkeydown="updateResult(event)"/></label>
+ <label>keyPress: <input type="text" id="keyPress" onkeypress="updateResult(event)"/></label>
+ <label>keyUp: <input type="text" id="keyUp" onkeyup="updateResult(event)"/></label>
+ <label>change: <input type="text" id="change" onchange="updateResult(event)"/></label>
+ </p>
+ <p>
+ <label>change:
+ <input type="checkbox" id="checkbox" value="checkbox thing" onchange="updateResult(event)"/>
+ </label>
+ </p>
+ <p>
+ <label>keyDown:
+ <textarea id="keyDownArea" onkeydown="updateResult(event)" rows="2" cols="15"></textarea>
+ </label>
+ <label>keyPress:
+ <textarea id="keyPressArea" onkeypress="updateResult(event)" rows="2" cols="15"></textarea>
+ </label>
+ <label>keyUp:
+ <textarea id="keyUpArea" onkeyup="updateResult(event)" rows="2" cols="15"></textarea>
+ </label>
+ </p>
+ <p>
+ <select id="selector" onchange="updateResult(event)">
+ <option value="foo">Foo</option>
+ <option value="bar">Bar</option>
+ </select>
+ </p>
+ </form>
+</div>
+
+<div id="formageddon">
+ <form action="#">
+ Key Up: <input type="text" id="keyUp" onkeyup="javascript:updateContent(this)"/><br/>
+ Key Down: <input type="text" id="keyDown" onkeydown="javascript:updateContent(this)"/><br/>
+ Key Press: <input type="text" id="keyPress" onkeypress="javascript:updateContent(this)"/><br/>
+ Change: <input type="text" id="change" onkeypress="javascript:displayMessage('change')"/><br/>
+ <textarea id="keyDownArea" onkeydown="javascript:updateContent(this)" rows="2" cols="15"></textarea>
+ <textarea id="keyPressArea" onkeypress="javascript:updateContent(this)" rows="2" cols="15"></textarea>
+ <textarea id="keyUpArea" onkeyup="javascript:updateContent(this)" rows="2" cols="15"></textarea>
+ <select id="selector" onchange="javascript:updateContent(this)">
+ <option value="foo">Foo</option>
+ <option value="bar">Bar</option>
+ </select>
+ <input type="checkbox" id="checkbox" value="checkbox thing" onchange="javascript:updateContent(this)"/>
+ <input id="clickField" type="text" onclick="document.getElementById('clickField').value='Clicked';" value="Hello"/>
+ <input id="doubleClickField" type="text" onclick="document.getElementById('doubleClickField').value='Clicked';" ondblclick="document.getElementById('doubleClickField').value='DoubleClicked';" oncontextmenu="document.getElementById('doubleClickField').value='ContextClicked'; return false;" value="DoubleHello"/>
+ <input id="clearMe" value="Something" onchange="displayMessage('Cleared')"/>
+ <input type="text" id="notDisplayed" style="display: none">
+ </form>
+</div>
+
+<div>
+ <form>
+ <input type="text" id="keyReporter" size="80"
+ onkeyup="appendMessage('up: ' + event.keyCode)"
+ onkeypress="appendMessage('press: ' + event.keyCode)"
+ onkeydown="displayMessage(''); appendMessage('down: ' + event.keyCode)" />
+ <input name="suppress" onkeydown="if (event.preventDefault) event.preventDefault(); event.returnValue = false; return false;" onkeypress="appendMessage('press');"/>
+ </form>
+</div>
+
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_columns.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_columns.html
new file mode 100644
index 0000000000..bc414cfc45
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_columns.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <style>
+ #columns {
+ column-count: 2;
+ -webkit-column-count: 2;
+ column-rule: 1px solid lightgray;
+ -webkit-column-rule: 1px solid lightgray;
+ border: 1px solid lightblue;
+ width: 450px;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="columns">
+ <div id="columns-inner" style="border: 1px solid red;" contenteditable="true">
+ <p id="before-image-1">Before image 1</p>
+ <p><img width="100px" height="30px" src="data:image/gif;base64,R0lGODlhAQABAIABAAD/AP///ywAAAAAAQABAAACAkQBADs="></p>
+ <p>After image 1</p>
+ <p>Before image 2</p>
+ <p><img width="100px" height="30px" src="data:image/gif;base64,R0lGODlhAQABAIABAAD/AP///ywAAAAAAQABAAACAkQBADs="></p>
+ <p>After image 2</p>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_cursor.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_cursor.html
new file mode 100644
index 0000000000..fdbd6fe7a8
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_cursor.html
@@ -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/. -->
+
+<!DOCTYPE html>
+<html id="html">
+ <head>
+ <title>Marionette tests for AccessibleCaret in cursor mode</title>
+ <style>
+ .block {
+ width: 10em;
+ height: 6em;
+ word-wrap: break-word;
+ overflow: auto;
+ }
+ </style>
+ </head>
+ <body>
+ <div>
+ <input id="input" value="ABCDEFGHI">
+ <input id="input-padding" style="padding: 1em;" value="ABCDEFGHI">
+ </div>
+ <br>
+ <div>
+ <textarea name="textarea" id="textarea" rows="4" cols="6">ABCDEFGHI</textarea>
+ <textarea id="textarea-one-line" rows="3">ABCDEFGHI</textarea>
+ </div>
+ <br>
+ <div class="block" contenteditable="true" id="contenteditable">ABCDEFGHI</div>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_display_none.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_display_none.html
new file mode 100644
index 0000000000..766f320011
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_display_none.html
@@ -0,0 +1,10 @@
+<!-- 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 id="html" style="display: none">
+ <body>
+ <div id="content">ABC DEF GHI</div>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe.html
new file mode 100644
index 0000000000..175d3c3d5c
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe.html
@@ -0,0 +1,15 @@
+<!-- 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 id="html">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Marionette tests for AccessibleCaret in selection mode (iframe)</title>
+ </head>
+ <body>
+ <iframe id="frame" src="test_carets_longtext.html" style="width: 10em; height: 8em;"></iframe>
+ <input id="input" value="ABC DEF GHI">
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll.html
new file mode 100644
index 0000000000..5f4b00e5bd
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <title>Bug 1657256: Test select word, scroll up, and drag AccessibleCaret.</title>
+ <style>
+ :root {
+ font: 16px/1.25 monospace;
+ }
+ </style>
+
+ <iframe id="iframe" src="test_carets_iframe_scroll_inner.html" style="width: 6em; height: 8em;"></iframe>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll_inner.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll_inner.html
new file mode 100644
index 0000000000..1087227007
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll_inner.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <style>
+ :root {
+ font: 16px/1.25 monospace;
+ }
+ </style>
+
+ <body id="bd">
+ AAAAAA
+ BBBBBB
+ CCCCCC
+ <span id="content">DDDDDD</span>
+ <span id="content2">EEEEEE</span>
+ FFFFFF
+ GGGGGG
+ HHHHHH
+ IIIIII
+ JJJJJJ
+ KKKKKK
+ LLLLLL
+ MMMMMM
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_key_scroll.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_key_scroll.html
new file mode 100644
index 0000000000..a42a342285
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_key_scroll.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html id="html">
+ <style>
+ :root {
+ font: 16px/1.25 monospace;
+ }
+ div {
+ width: 100px;
+ height: 5000px;
+ border: 5px solid blue;
+ }
+ </style>
+
+ <div>
+ <span id="content">AAAAA</span><br>
+ <span id="content2">BBBBB</span>
+ </div>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_longtext.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_longtext.html
new file mode 100644
index 0000000000..7e2495509b
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_longtext.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <title>Bug 1094072: Orientation change test for AccessibleCaret positions</title>
+ </head>
+ <body id="bd">
+ <h3 id="longtext">long long text for orientation change test long long text for orientation change test long long text for orientation change test long long text for orientation change test</h3>
+ <div contenteditable="true" id="bottomtext">bottom text</div>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_multipleline.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_multipleline.html
new file mode 100644
index 0000000000..fbbefbebcb
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_multipleline.html
@@ -0,0 +1,18 @@
+<!-- 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 id="html">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Bug 1019441: Marionette tests for AccessibleCaret (multiple lines)</title>
+ </head>
+ <body>
+ <div><textarea id="textarea2" style="width: 10em; height: 6em; overflow: auto;">First Line&#13;&#10;&#13;&#10;Second Line&#13;&#10;&#13;&#10;Third Line</textarea></div>
+ <br>
+ <div style="width: 10em; height: 6em; overflow: auto;" id="contenteditable2" contenteditable="true">First Line<br><br>Second Line<br><br>Third Line</div>
+ <br>
+ <div style="width: 10em; height: 6em; overflow: auto;" id="content2">First Line<br><br>Second Line<br><br>Third Line</div>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_multiplerange.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_multiplerange.html
new file mode 100644
index 0000000000..9b9bbe9e9f
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_multiplerange.html
@@ -0,0 +1,19 @@
+<html>
+<style>
+h4 {
+ user-select: none;
+}
+</style>
+<body id=bd>
+<h3 id=sel1>user can select this 1</h3>
+<h3 id=sel2>user can select this 2</h3>
+<h3 id=sel3>user can select this 3</h3>
+<h4 id=nonsel1>user cannot select this 1</h4>
+<h4 id=nonsel2>user cannot select this 2</h4>
+<h3 id=sel4>user can select this 4</h3>
+<h3 id=sel5>user can select this 5</h3>
+<h4 id=nonsel3>user cannot select this 3</h4>
+<h3 id=sel6>user can select this 6</h3>
+<h3 id=sel7>user can select this 7</h3>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_selection.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_selection.html
new file mode 100644
index 0000000000..9098ed447c
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_selection.html
@@ -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/. -->
+
+<!DOCTYPE html>
+<html id="html">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Marionette tests for AccessibleCaret in selection mode</title>
+ <style>
+ body {
+ /* Clicking on a point outside of viewport will trigger
+ marionette_driver.errors.MoveTargetOutOfBoundsException. Increase the
+ margin to prevent that. */
+ margin: 30px;
+ }
+ .block {
+ width: 10em;
+ height: 4em;
+ word-wrap: break-word;
+ overflow: auto;
+ }
+ </style>
+ </head>
+ <body>
+ <div>
+ <input id="input" value="ABC DEF GHI">
+ <input id="input-padding" style="padding: 1em;" value="ABC DEF GHI">
+
+ <!-- To successfully select 'B's when 'A' is selected, use sufficient
+ spaces between 'A's and 'B's to avoid the second caret covers 'B's. -->
+ <input size="16" id="input-size" value="AAAAAAAA BBBBBBBB">
+ </div>
+ <br>
+ <div>
+ <textarea id="textarea" rows="4" cols="8">ABC DEF GHI JKL MNO PQR</textarea>
+ <textarea id="textarea-disabled" rows="4" cols="8" disabled>ABC DEF GHI JKL MNO PQR</textarea>
+ <textarea id="textarea-one-line" rows="4" cols="12">ABC DEF GHI</textarea>
+ </div>
+ <br>
+ <div><textarea dir="rtl" id="textarea-rtl" rows="8" cols="8">موزيلا فيرفكس موزيلا فيرفكس</textarea></div>
+ <br>
+ <div class="block" contenteditable="true" id="contenteditable">ABC DEF GHI</div>
+ <br>
+ <div class="block" id="content">ABC DEF GHI</div>
+ <br>
+ <div style="user-select: none;" id="non-selectable">Non-selectable</div>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/layout/test_carets_svg_shapes.html b/testing/marionette/harness/marionette_harness/www/layout/test_carets_svg_shapes.html
new file mode 100644
index 0000000000..ea3ad4ecf6
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/layout/test_carets_svg_shapes.html
@@ -0,0 +1,12 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<!DOCTYPE html>
+<html>
+ <body>
+ <svg xmlns="http://www.w3.org/2000/svg" id="svg-element" width="200" height="200">
+ <rect id="rect" x="100" y="100" width="20" height="20"></rect>
+ </svg>
+ <p id="text">ABC DEF GHI</p>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/navigation_pushstate.html b/testing/marionette/harness/marionette_harness/www/navigation_pushstate.html
new file mode 100644
index 0000000000..fbde792d8c
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/navigation_pushstate.html
@@ -0,0 +1,20 @@
+<!-- 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>Navigation by manipulating the browser history</title>
+ <script type="text/javascript">
+ function forward() {
+ let stateObj = { foo: "bar" };
+ history.pushState(stateObj, "", "navigation_pushstate_target.html");
+ }
+ </script>
+</head>
+
+<body>
+ <p>Navigate <a onclick="javascript:forward();" id="forward">forward</a></p>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/navigation_pushstate_target.html b/testing/marionette/harness/marionette_harness/www/navigation_pushstate_target.html
new file mode 100644
index 0000000000..153d0a657f
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/navigation_pushstate_target.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>
+</head>
+
+<body>
+ <p id="target">Pushstate target</p>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/nestedElements.html b/testing/marionette/harness/marionette_harness/www/nestedElements.html
new file mode 100644
index 0000000000..618bf3231b
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/nestedElements.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<a href="1.html">hello world</a>
+<a href="1.html">hello world</a><a href="1.html">hello world</a>
+<div name="div1">
+ <a href="2.html" name="link1">hello world</a>
+ <a href="2.html" name="link2">hello world</a>
+</div>
+
+<a href="1.html">hello world</a><a href="1.html">hello world</a><a href="1.html">hello world</a>
diff --git a/testing/marionette/harness/marionette_harness/www/reftest/mostly-teal-700x700.html b/testing/marionette/harness/marionette_harness/www/reftest/mostly-teal-700x700.html
new file mode 100644
index 0000000000..a5aa12d0d2
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/reftest/mostly-teal-700x700.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<style>
+ * {
+ margin: 0;
+ padding: 0;
+ }
+
+ html {
+ background: teal;
+ }
+
+ div {
+ position: absolute;
+ top: 600px;
+ left: 600px;
+ height: 100px;
+ width: 100px;
+ background: orange;
+ }
+</style>
+<div></div>
diff --git a/testing/marionette/harness/marionette_harness/www/reftest/teal-700x700.html b/testing/marionette/harness/marionette_harness/www/reftest/teal-700x700.html
new file mode 100644
index 0000000000..e441e88e6d
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/reftest/teal-700x700.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<style>
+ * {
+ margin: 0;
+ padding: 0;
+ }
+
+ html {
+ background: teal;
+ }
+
+ div {
+ position: absolute;
+ top: 600px;
+ left: 600px;
+ height: 100px;
+ width: 100px;
+ background: transparent;
+ }
+</style>
+<div></div>
diff --git a/testing/marionette/harness/marionette_harness/www/resultPage.html b/testing/marionette/harness/marionette_harness/www/resultPage.html
new file mode 100644
index 0000000000..6e2fea9a14
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/resultPage.html
@@ -0,0 +1,16 @@
+<!-- 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/. -->
+
+<html>
+<head>
+ <title>We Arrive Here</title>
+</head>
+<body>
+
+
+<div>
+ <input type='text' id='email'/>
+</div>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/serviceworker/install_serviceworker.html b/testing/marionette/harness/marionette_harness/www/serviceworker/install_serviceworker.html
new file mode 100644
index 0000000000..572a05c2bc
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/serviceworker/install_serviceworker.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Install Service Worker</title>
+ </head>
+ <body>
+ <script>
+ navigator.serviceWorker.register("serviceworker.js");
+ </script>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/serviceworker/serviceworker.js b/testing/marionette/harness/marionette_harness/www/serviceworker/serviceworker.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/serviceworker/serviceworker.js
diff --git a/testing/marionette/harness/marionette_harness/www/shim.js b/testing/marionette/harness/marionette_harness/www/shim.js
new file mode 100644
index 0000000000..9a8e09d6b6
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/shim.js
@@ -0,0 +1,297 @@
+/**
+ * mouse_event_shim.js: generate mouse events from touch events.
+ *
+ * This library listens for touch events and generates mousedown, mousemove
+ * mouseup, and click events to match them. It captures and dicards any
+ * real mouse events (non-synthetic events with isTrusted true) that are
+ * send by gecko so that there are not duplicates.
+ *
+ * This library does emit mouseover/mouseout and mouseenter/mouseleave
+ * events. You can turn them off by setting MouseEventShim.trackMouseMoves to
+ * false. This means that mousemove events will always have the same target
+ * as the mousedown even that began the series. You can also call
+ * MouseEventShim.setCapture() from a mousedown event handler to prevent
+ * mouse tracking until the next mouseup event.
+ *
+ * This library does not support multi-touch but should be sufficient
+ * to do drags based on mousedown/mousemove/mouseup events.
+ *
+ * This library does not emit dblclick events or contextmenu events
+ */
+
+"use strict";
+
+(function () {
+ // Make sure we don't run more than once
+ if (MouseEventShim) {
+ return;
+ }
+
+ // Bail if we're not on running on a platform that sends touch
+ // events. We don't need the shim code for mouse events.
+ try {
+ document.createEvent("TouchEvent");
+ } catch (e) {
+ return;
+ }
+
+ let starttouch; // The Touch object that we started with
+ let target; // The element the touch is currently over
+ let emitclick; // Will we be sending a click event after mouseup?
+
+ // Use capturing listeners to discard all mouse events from gecko
+ window.addEventListener("mousedown", discardEvent, true);
+ window.addEventListener("mouseup", discardEvent, true);
+ window.addEventListener("mousemove", discardEvent, true);
+ window.addEventListener("click", discardEvent, true);
+
+ function discardEvent(e) {
+ if (e.isTrusted) {
+ e.stopImmediatePropagation(); // so it goes no further
+ if (e.type === "click") {
+ e.preventDefault();
+ } // so it doesn't trigger a change event
+ }
+ }
+
+ // Listen for touch events that bubble up to the window.
+ // If other code has called stopPropagation on the touch events
+ // then we'll never see them. Also, we'll honor the defaultPrevented
+ // state of the event and will not generate synthetic mouse events
+ window.addEventListener("touchstart", handleTouchStart);
+ window.addEventListener("touchmove", handleTouchMove);
+ window.addEventListener("touchend", handleTouchEnd);
+ window.addEventListener("touchcancel", handleTouchEnd); // Same as touchend
+
+ function handleTouchStart(e) {
+ // If we're already handling a touch, ignore this one
+ if (starttouch) {
+ return;
+ }
+
+ // Ignore any event that has already been prevented
+ if (e.defaultPrevented) {
+ return;
+ }
+
+ // Sometimes an unknown gecko bug causes us to get a touchstart event
+ // for an iframe target that we can't use because it is cross origin.
+ // Don't start handling a touch in that case
+ try {
+ e.changedTouches[0].target.ownerDocument;
+ } catch (e) {
+ // Ignore the event if we can't see the properties of the target
+ return;
+ }
+
+ // If there is more than one simultaneous touch, ignore all but the first
+ starttouch = e.changedTouches[0];
+ target = starttouch.target;
+ emitclick = true;
+
+ // Move to the position of the touch
+ emitEvent("mousemove", target, starttouch);
+
+ // Now send a synthetic mousedown
+ let result = emitEvent("mousedown", target, starttouch);
+
+ // If the mousedown was prevented, pass that on to the touch event.
+ // And remember not to send a click event
+ if (!result) {
+ e.preventDefault();
+ emitclick = false;
+ }
+ }
+
+ function handleTouchEnd(e) {
+ if (!starttouch) {
+ return;
+ }
+
+ // End a MouseEventShim.setCapture() call
+ if (MouseEventShim.capturing) {
+ MouseEventShim.capturing = false;
+ MouseEventShim.captureTarget = null;
+ }
+
+ for (let i = 0; i < e.changedTouches.length; i++) {
+ let touch = e.changedTouches[i];
+ // If the ended touch does not have the same id, skip it
+ if (touch.identifier !== starttouch.identifier) {
+ continue;
+ }
+
+ emitEvent("mouseup", target, touch);
+
+ // If target is still the same element we started and the touch did not
+ // move more than the threshold and if the user did not prevent
+ // the mousedown, then send a click event, too.
+ if (emitclick) {
+ emitEvent("click", starttouch.target, touch);
+ }
+
+ starttouch = null;
+ return;
+ }
+ }
+
+ function handleTouchMove(e) {
+ if (!starttouch) {
+ return;
+ }
+
+ for (let i = 0; i < e.changedTouches.length; i++) {
+ let touch = e.changedTouches[i];
+ // If the ended touch does not have the same id, skip it
+ if (touch.identifier !== starttouch.identifier) {
+ continue;
+ }
+
+ // Don't send a mousemove if the touchmove was prevented
+ if (e.defaultPrevented) {
+ return;
+ }
+
+ // See if we've moved too much to emit a click event
+ let dx = Math.abs(touch.screenX - starttouch.screenX);
+ let dy = Math.abs(touch.screenY - starttouch.screenY);
+ if (
+ dx > MouseEventShim.dragThresholdX ||
+ dy > MouseEventShim.dragThresholdY
+ ) {
+ emitclick = false;
+ }
+
+ let tracking =
+ MouseEventShim.trackMouseMoves && !MouseEventShim.capturing;
+
+ let oldtarget;
+ let newtarget;
+ if (tracking) {
+ // If the touch point moves, then the element it is over
+ // may have changed as well. Note that calling elementFromPoint()
+ // forces a layout if one is needed.
+ // XXX: how expensive is it to do this on each touchmove?
+ // Can we listen for (non-standard) touchleave events instead?
+ oldtarget = target;
+ newtarget = document.elementFromPoint(touch.clientX, touch.clientY);
+ if (newtarget === null) {
+ // this can happen as the touch is moving off of the screen, e.g.
+ newtarget = oldtarget;
+ }
+ if (newtarget !== oldtarget) {
+ leave(oldtarget, newtarget, touch); // mouseout, mouseleave
+ target = newtarget;
+ }
+ } else if (MouseEventShim.captureTarget) {
+ target = MouseEventShim.captureTarget;
+ }
+
+ emitEvent("mousemove", target, touch);
+
+ if (tracking && newtarget !== oldtarget) {
+ enter(newtarget, oldtarget, touch); // mouseover, mouseenter
+ }
+ }
+ }
+
+ // Return true if element a contains element b
+ function contains(a, b) {
+ return (a.compareDocumentPosition(b) & 16) !== 0;
+ }
+
+ // A touch has left oldtarget and entered newtarget
+ // Send out all the events that are required
+ function leave(oldtarget, newtarget, touch) {
+ emitEvent("mouseout", oldtarget, touch, newtarget);
+
+ // If the touch has actually left oldtarget (and has not just moved
+ // into a child of oldtarget) send a mouseleave event. mouseleave
+ // events don't bubble, so we have to repeat this up the hierarchy.
+ for (let e = oldtarget; !contains(e, newtarget); e = e.parentNode) {
+ emitEvent("mouseleave", e, touch, newtarget);
+ }
+ }
+
+ // A touch has entered newtarget from oldtarget
+ // Send out all the events that are required.
+ function enter(newtarget, oldtarget, touch) {
+ emitEvent("mouseover", newtarget, touch, oldtarget);
+
+ // Emit non-bubbling mouseenter events if the touch actually entered
+ // newtarget and wasn't already in some child of it
+ for (let e = newtarget; !contains(e, oldtarget); e = e.parentNode) {
+ emitEvent("mouseenter", e, touch, oldtarget);
+ }
+ }
+
+ function emitEvent(type, target, touch, relatedTarget) {
+ let synthetic = document.createEvent("MouseEvents");
+ let bubbles = type !== "mouseenter" && type !== "mouseleave";
+ let count =
+ type === "mousedown" || type === "mouseup" || type === "click" ? 1 : 0;
+
+ synthetic.initMouseEvent(
+ type,
+ bubbles, // canBubble
+ true, // cancelable
+ window,
+ count, // detail: click count
+ touch.screenX,
+ touch.screenY,
+ touch.clientX,
+ touch.clientY,
+ false, // ctrlKey: we don't have one
+ false, // altKey: we don't have one
+ false, // shiftKey: we don't have one
+ false, // metaKey: we don't have one
+ 0, // we're simulating the left button
+ relatedTarget || null
+ );
+
+ try {
+ return target.dispatchEvent(synthetic);
+ } catch (e) {
+ console.warn("Exception calling dispatchEvent", type, e);
+ return true;
+ }
+ }
+})();
+
+const MouseEventShim = {
+ // It is a known gecko bug that synthetic events have timestamps measured
+ // in microseconds while regular events have timestamps measured in
+ // milliseconds. This utility function returns a the timestamp converted
+ // to milliseconds, if necessary.
+ getEventTimestamp(e) {
+ if (e.isTrusted) {
+ // XXX: Are real events always trusted?
+ return e.timeStamp;
+ }
+ return e.timeStamp / 1000;
+ },
+
+ // Set this to false if you don't care about mouseover/out events
+ // and don't want the target of mousemove events to follow the touch
+ trackMouseMoves: true,
+
+ // Call this function from a mousedown event handler if you want to guarantee
+ // that the mousemove and mouseup events will go to the same element
+ // as the mousedown even if they leave the bounds of the element. This is
+ // like setting trackMouseMoves to false for just one drag. It is a
+ // substitute for event.target.setCapture(true)
+ setCapture(target) {
+ this.capturing = true; // Will be set back to false on mouseup
+ if (target) {
+ this.captureTarget = target;
+ }
+ },
+
+ capturing: false,
+
+ // Keep these in sync with ui.dragThresholdX and ui.dragThresholdY prefs.
+ // If a touch ever moves more than this many pixels from its starting point
+ // then we will not synthesize a click event when the touch ends.
+ dragThresholdX: 25,
+ dragThresholdY: 25,
+};
diff --git a/testing/marionette/harness/marionette_harness/www/slow_resource.html b/testing/marionette/harness/marionette_harness/www/slow_resource.html
new file mode 100644
index 0000000000..b87d9f4b86
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/slow_resource.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>Slow loading resource</title>
+</head>
+<body>
+ <img src="/slow?delay=4" id="slow" />
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test.html b/testing/marionette/harness/marionette_harness/www/test.html
new file mode 100644
index 0000000000..20689a6d59
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test.html
@@ -0,0 +1,43 @@
+<!-- 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>
+<style>
+input[type=text], input[type=button] {
+ appearance: none;
+}
+</style>
+</head>
+<body>
+ <h1 id="testh1">Test Page</h1>
+ <script type="text/javascript">
+ window.ready = true;
+ function addDelayedElement() {
+ setTimeout(createDiv, 2000);
+ function createDiv() {
+ let newDiv = document.createElement("div");
+ newDiv.id = "newDiv";
+ let newContent = document.createTextNode("I am a newly created div!");
+ newDiv.appendChild(newContent);
+ document.body.appendChild(newDiv);
+ }
+ }
+ function clicked() {
+ let link = document.getElementById("mozLink");
+ link.innerHTML = "Clicked";
+ }
+ </script>
+ <a href="#" id="mozLink" class="linkClass" onclick="clicked()">Click me!</a>
+ <div id="testDiv">
+ <a href="#" id="divLink" class="linkClass" onclick="clicked()">Div click me!</a>
+ <a href="#" id="divLink2" class="linkClass" onclick="clicked()">Div click me!</a>
+ </div>
+ <input name="myInput" type="text" value="asdf"/>
+ <input name="myCheckBox" type="checkbox" />
+ <input id="createDivButton" type="button" value="create a div" onclick="addDelayedElement()" />
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_accessibility.html b/testing/marionette/harness/marionette_harness/www/test_accessibility.html
new file mode 100644
index 0000000000..8cc9fd6493
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_accessibility.html
@@ -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/. -->
+
+<!DOCTYPE html>
+
+<html>
+<meta charset="UTF-8">
+<head>
+<title>Marionette Test</title>
+</head>
+<body>
+ <button id="button1">button1</button>
+ <button id="button2" aria-label="button2"></button>
+ <span id="button3">I am a bad button with no accessible</span>
+ <h1 id="button4">I am a bad button that is actually a header</h1>
+ <h1 id="button5">
+ I am a bad button that is actually an actionable header with a listener
+ </h1>
+ <button id="button6"></button>
+ <button id="button7" aria-hidden="true">button7</button>
+ <div aria-hidden="true">
+ <button id="button8">button8</button>
+ </div>
+ <button id="button9" style="position:absolute;left:-100px;top:-455px;">
+ button9
+ </button>
+ <button id="button10" style="visibility:hidden;">
+ button10
+ </button>
+ <span id="no_accessible_but_displayed">I have no accessible object</span>
+ <button id="button11" disabled>button11</button>
+ <button id="button12" aria-disabled="true">button12</button>
+ <span id="no_accessible_but_disabled" disabled>I have no accessible object</span>
+ <span id="button13" tabindex="0" role="button" aria-label="Span button">Span button</span>
+ <span id="button14" role="button" aria-label="Span button">Unexplorable Span button</span>
+ <button id="button15" style="pointer-events:none;">button15</button>
+ <div style="pointer-events:none;">
+ <button id="button16">button16</button>
+ </div>
+ <div style="pointer-events:none;">
+ <button style="pointer-events:all;" id="button17">button17</button>
+ </div>
+ <input id="input1" title="My Input 1" name="myInput1" type="text" value="asdf"/>
+ <select>
+ <option id="option1" value="val1">Val1</option>
+ <option id="option2" value="val2" selected>Val2</option>
+ </select>
+ <script>
+ 'use strict';
+ document.getElementById('button5').addEventListener('click', function() {
+ // A pseudo button that has a listener but is missing button semantics.
+ return true;
+ });
+ </script>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_clearing.html b/testing/marionette/harness/marionette_harness/www/test_clearing.html
new file mode 100644
index 0000000000..2aa3c6a21f
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_clearing.html
@@ -0,0 +1,24 @@
+<html>
+ <!-- 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/. -->
+ <body>
+ <input id="writableTextInput" type="text" value="Test"/>
+
+ <input id="readOnlyTextInput" type="text" readonly value="Test"/>
+
+ <input id="textInputnotenabled" type="text" disabled="true" value="Test"/>
+
+ <textarea id="writableTextArea" rows="2" cols="20">
+ This is a sample text area which is supposed to be cleared
+ </textarea>
+
+ <textarea id="textAreaReadOnly" readonly rows="5" cols="20">
+ text area which is not supposed to be cleared</textarea>
+
+ <textarea rows="5" id="textAreaNotenabled" disabled="true" cols="20">
+ text area which is not supposed to be cleared</textarea>
+
+ <div id="content-editable" contentEditable="true">This is a contentEditable area</div>
+ </body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_dynamic.html b/testing/marionette/harness/marionette_harness/www/test_dynamic.html
new file mode 100644
index 0000000000..504e7e74ba
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_dynamic.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+ <head>
+ <title></title>
+ <script type="text/javascript">
+ let next = 0;
+
+ function addMore() {
+ let box = document.createElement('DIV');
+ box.id = 'box' + next++;
+ box.className = 'redbox';
+ box.style.width = '150px';
+ box.style.height = '150px';
+ box.style.backgroundColor = 'red';
+ box.style.border = '1px solid black';
+ box.style.margin = '5px';
+ window.setTimeout(function() {
+ document.body.appendChild(box);
+ }, 1000);
+ }
+
+ function reveal() {
+ let elem = document.getElementById('revealed');
+ window.setTimeout(function() {
+ elem.style.display = '';
+ }, 1000);
+ }
+ </script>
+ </head>
+ <body>
+ <input id="adder" type="button" value="Add a box!" onclick="addMore()"/>
+
+ <input id="reveal" type="button" value="Reveal a new input" onclick="reveal();" />
+
+ <input id="revealed" style="display:none;" />
+ </body>
+ </html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_iframe.html b/testing/marionette/harness/marionette_harness/www/test_iframe.html
new file mode 100644
index 0000000000..b323ace679
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_iframe.html
@@ -0,0 +1,16 @@
+<!-- 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 IFrame Test</title>
+</head>
+<body>
+ <h1 id="iframe_page_heading">This is the heading</h1>
+
+ <iframe src="test.html" id="test_iframe"></iframe>
+ <iframe src="test.html" id="test_iframe" name="test_iframe_name"></iframe>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_inner_iframe.html b/testing/marionette/harness/marionette_harness/www/test_inner_iframe.html
new file mode 100644
index 0000000000..8c9810d0bb
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_inner_iframe.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>Inner Iframe</title>
+</head>
+<body>
+ <iframe src="test.html" id="inner_frame"></iframe>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_nested_iframe.html b/testing/marionette/harness/marionette_harness/www/test_nested_iframe.html
new file mode 100644
index 0000000000..49ac1b0ba5
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_nested_iframe.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 IFrame Test</title>
+</head>
+<body>
+ <iframe src="test_inner_iframe.html" id="test_iframe"></iframe>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_oop_1.html b/testing/marionette/harness/marionette_harness/www/test_oop_1.html
new file mode 100644
index 0000000000..29add714cd
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_oop_1.html
@@ -0,0 +1,14 @@
+<!-- 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>OOP Test Frame 1</title>
+</head>
+<body>
+ <h1 id="testh1">OOP Test Frame 1</h1>
+ Hello!
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_oop_2.html b/testing/marionette/harness/marionette_harness/www/test_oop_2.html
new file mode 100644
index 0000000000..6e5a4962fb
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_oop_2.html
@@ -0,0 +1,14 @@
+<!-- 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>OOP Test Frame 2</title>
+</head>
+<body>
+ <h1 id="testh1">OOP Test Frame 2</h1>
+ Hello!
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/test_windows.html b/testing/marionette/harness/marionette_harness/www/test_windows.html
new file mode 100644
index 0000000000..f3759990c0
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/test_windows.html
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <title>XHTML Test Page</title>
+</head>
+<body>
+ <p><a href="resultPage.html" onClick='javascript:window.open("resultPage.html",null, "menubar=0,location=1,resizable=1,scrollbars=1,status=0,width=700,height=375");' name="windowOne">Open new window</a></p>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/update/complete.mar b/testing/marionette/harness/marionette_harness/www/update/complete.mar
new file mode 100644
index 0000000000..375fd7bd08
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/update/complete.mar
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/update/complete.mar.headers b/testing/marionette/harness/marionette_harness/www/update/complete.mar.headers
new file mode 100644
index 0000000000..bcf051e2c7
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/update/complete.mar.headers
@@ -0,0 +1 @@
+Content-Length: 86612
diff --git a/testing/marionette/harness/marionette_harness/www/visibility.html b/testing/marionette/harness/marionette_harness/www/visibility.html
new file mode 100644
index 0000000000..2296f3cd46
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/visibility.html
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <!-- 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/. -->
+<head>
+ <title>Testing Visibility</title>
+</head>
+
+<body>
+
+<div>
+ <span id="hideMe" onclick="this.style.display = 'none';">Click to hide me.</span>
+</div>
+
+<div id="zero" style="width:0;height:0">
+ <div>
+ <img src="map.png">
+ </div>
+</div>
+
+<p id="suppressedParagraph" style="display: none">A paragraph suppressed using CSS display=none</p>
+
+<div>
+ <p id="displayed">Displayed</p>
+
+ <form action="#"><input type="hidden" name="hidden" /> </form>
+
+ <p id="none" style="display: none;">Display set to none</p>
+
+ <p id="hidden" style="visibility: hidden;">Hidden</p>
+
+ <div id="hiddenparent" style="height: 2em; display: none;">
+ <div id="hiddenchild">
+ <a href="#" id="hiddenlink">ok</a>
+ </div>
+ </div>
+
+ <div style="visibility: hidden;">
+ <span>
+ <input id="unclickable" />
+ <input type="checkbox" id="untogglable" checked="checked" />Check box you can't see
+ </span>
+ </div>
+
+ <p id="outer" style="visibility: hidden">A <b id="visibleSubElement" style="visibility: visible">sub-element that is explicitly visible</b> using CSS visibility=visible</p>
+</div>
+
+<input type='text' id='notDisplayed' style='display:none'>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/white.png b/testing/marionette/harness/marionette_harness/www/white.png
new file mode 100644
index 0000000000..8a68c11548
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/white.png
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/www/windowHandles.html b/testing/marionette/harness/marionette_harness/www/windowHandles.html
new file mode 100644
index 0000000000..bcd0b08dc3
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/windowHandles.html
@@ -0,0 +1,16 @@
+<!-- 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 New Tab Link</title>
+</head>
+<body>
+ <a href="empty.html" id="new-tab" target="_blank">New Tab</a>
+ <a href="about:blank" id="new-blank-tab" target="_blank">New blank Tab</a>
+
+ <a href="" id="new-window" onClick='javascript:window.open("empty.html", null, "location=1,toolbar=1");'>New Window</a>
+</body>
+</html>
diff --git a/testing/marionette/harness/marionette_harness/www/xhtmlTest.html b/testing/marionette/harness/marionette_harness/www/xhtmlTest.html
new file mode 100644
index 0000000000..30940c709e
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/www/xhtmlTest.html
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <!-- 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/. -->
+<head>
+ <title>XHTML Test Page</title>
+</head>
+<body>
+<div class="navigation">
+ <p><a href="resultPage.html" target="result" name="windowOne">Open new window</a></p>
+ <p><a href="iframes.html" target="_blank" name="windowTwo">Create a new anonymous window</a></p>
+ <p><a href="test_iframe.html" name="sameWindow">Open page with iframes in same window</a></p>
+ <p><a href="test.html" target="result" name="windowThree">Open a window with a close button</a></p>
+</div>
+
+<a name="notext"><b></b></a>
+
+<div class="content">
+ <h1 class="header">XHTML Might Be The Future</h1>
+
+ <p>If you'd like to go elsewhere then <a href="resultPage.html">click me</a>.</p>
+
+ <p>Alternatively, <a href="resultPage.html" id="linkId">this goes to the same place</a>.</p>
+
+ <form name="someForm">
+ <input id="username" type="text" value="change"/>
+ </form>
+
+ This link has the same text as another link: <a href="resultPage.html">click me</a>.
+</div>
+
+<div class="extraDiv">Another div starts here.<p/>
+ <h2 class="nameA nameBnoise nameC">An H2 title</h2>
+ <p class="nameC">Some more text</p>
+</div>
+
+<div>
+ <a id="id1" href="#">Foo</a>
+ <ul id="id2" />
+ <span id="id3"/>
+</div>
+
+<div>
+ <table id="table" ></table>
+</div>
+
+<span id="amazing">
+<div>
+ <div>
+ <div>
+ <span/>
+ <a>I have width</a>
+ </div>
+ </div>
+</div>
+</span>
+
+<a name="text" />
+<p id="spaces"> </p>
+<p id="empty"></p>
+<a href="foo" id="linkWithEqualsSign">Link=equalssign</a>
+
+<p class=" spaceAround ">Spaced out</p>
+
+<span id="my_span">
+ <div>first_div</div>
+ <div>second_div</div>
+ <span>first_span</span>
+ <span>second_span</span>
+</span>
+
+<div id="parent">I'm a parent
+ <div id="child">I'm a child</div>
+</div>
+
+<div id="only-exists-on-xhtmltest">Woo woo</div>
+</body>
+</html>
diff --git a/testing/marionette/harness/requirements.txt b/testing/marionette/harness/requirements.txt
new file mode 100644
index 0000000000..aa307b196d
--- /dev/null
+++ b/testing/marionette/harness/requirements.txt
@@ -0,0 +1,15 @@
+browsermob-proxy >= 0.8.0
+manifestparser >= 1.1
+marionette-driver >= 3.0.0
+mozcrash >= 2.0
+mozdevice >= 4.0.0,<5
+mozinfo >= 1.0.0
+mozlog >= 6.0
+moznetwork >= 0.27
+mozprocess >= 1.0.0
+mozprofile >= 2.2.0
+mozrunner >= 7.4.0
+moztest >= 0.8
+mozversion >= 2.1.0
+six
+wptserve >= 2.0.0
diff --git a/testing/marionette/harness/setup.py b/testing/marionette/harness/setup.py
new file mode 100644
index 0000000000..fd43cdb969
--- /dev/null
+++ b/testing/marionette/harness/setup.py
@@ -0,0 +1,58 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import re
+
+from setuptools import find_packages, setup
+
+THIS_DIR = os.path.dirname(os.path.realpath(__name__))
+
+
+def read(*parts):
+ with open(os.path.join(THIS_DIR, *parts)) as f:
+ return f.read()
+
+
+def get_version():
+ return re.findall(
+ '__version__ = "([\d\.]+)"', read("marionette_harness", "__init__.py"), re.M
+ )[0]
+
+
+setup(
+ name="marionette-harness",
+ version=get_version(),
+ description="Marionette test automation harness",
+ long_description=open("README.rst").read(),
+ # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Operating System :: MacOS :: MacOS X",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX",
+ "Topic :: Software Development :: Quality Assurance",
+ "Topic :: Software Development :: Testing",
+ "Topic :: Utilities",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2.7",
+ ],
+ keywords="mozilla",
+ author="Auto-tools",
+ author_email="dev-webdriver@mozilla.org",
+ url="https://wiki.mozilla.org/Auto-tools/Projects/Marionette",
+ license="Mozilla Public License 2.0 (MPL 2.0)",
+ packages=find_packages(),
+ # Needed to include package data as specified in MANIFEST.in
+ include_package_data=True,
+ install_requires=read("requirements.txt").splitlines(),
+ zip_safe=False,
+ entry_points="""
+ # -*- Entry points: -*-
+ [console_scripts]
+ marionette = marionette_harness.runtests:cli
+ """,
+)