summaryrefslogtreecommitdiffstats
path: root/testing/marionette
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette')
-rw-r--r--testing/marionette/client/MANIFEST.in2
-rw-r--r--testing/marionette/client/docs/Makefile153
-rw-r--r--testing/marionette/client/docs/advanced/actions.rst21
-rw-r--r--testing/marionette/client/docs/advanced/debug.rst35
-rw-r--r--testing/marionette/client/docs/advanced/findelement.rst87
-rw-r--r--testing/marionette/client/docs/advanced/landing.rst13
-rw-r--r--testing/marionette/client/docs/advanced/stale.rst76
-rw-r--r--testing/marionette/client/docs/basics.rst195
-rw-r--r--testing/marionette/client/docs/conf.py274
-rw-r--r--testing/marionette/client/docs/index.rst16
-rw-r--r--testing/marionette/client/docs/interactive.rst52
-rw-r--r--testing/marionette/client/docs/make.bat190
-rw-r--r--testing/marionette/client/docs/reference.rst66
-rw-r--r--testing/marionette/client/marionette_driver/__init__.py22
-rw-r--r--testing/marionette/client/marionette_driver/addons.py76
-rw-r--r--testing/marionette/client/marionette_driver/by.py25
-rw-r--r--testing/marionette/client/marionette_driver/date_time_value.py49
-rw-r--r--testing/marionette/client/marionette_driver/decorators.py79
-rw-r--r--testing/marionette/client/marionette_driver/errors.py206
-rw-r--r--testing/marionette/client/marionette_driver/expected.py315
-rw-r--r--testing/marionette/client/marionette_driver/geckoinstance.py663
-rw-r--r--testing/marionette/client/marionette_driver/keys.py87
-rw-r--r--testing/marionette/client/marionette_driver/localization.py54
-rw-r--r--testing/marionette/client/marionette_driver/marionette.py2183
-rw-r--r--testing/marionette/client/marionette_driver/timeout.py103
-rw-r--r--testing/marionette/client/marionette_driver/transport.py409
-rw-r--r--testing/marionette/client/marionette_driver/wait.py175
-rw-r--r--testing/marionette/client/marionette_driver/webauthn.py63
-rw-r--r--testing/marionette/client/requirements.txt3
-rw-r--r--testing/marionette/client/setup.py54
-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
-rw-r--r--testing/marionette/mach_commands.py113
-rw-r--r--testing/marionette/mach_test_package_commands.py66
-rw-r--r--testing/marionette/moz.build13
220 files changed, 24370 insertions, 0 deletions
diff --git a/testing/marionette/client/MANIFEST.in b/testing/marionette/client/MANIFEST.in
new file mode 100644
index 0000000000..cf628b039c
--- /dev/null
+++ b/testing/marionette/client/MANIFEST.in
@@ -0,0 +1,2 @@
+exclude MANIFEST.in
+include requirements.txt
diff --git a/testing/marionette/client/docs/Makefile b/testing/marionette/client/docs/Makefile
new file mode 100644
index 0000000000..f3d89d6d47
--- /dev/null
+++ b/testing/marionette/client/docs/Makefile
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MarionettePythonClient.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MarionettePythonClient.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/MarionettePythonClient"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/MarionettePythonClient"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/testing/marionette/client/docs/advanced/actions.rst b/testing/marionette/client/docs/advanced/actions.rst
new file mode 100644
index 0000000000..c767bdecdc
--- /dev/null
+++ b/testing/marionette/client/docs/advanced/actions.rst
@@ -0,0 +1,21 @@
+Actions
+=======
+
+.. py:currentmodule:: marionette_driver.marionette
+
+Action Sequences
+----------------
+
+:class:`Actions` are designed as a way to simulate user input like a keyboard
+or a pointer device as closely as possible. For multiple interactions an
+action sequence can be used::
+
+ element = marionette.find_element("id", "input")
+ element.click()
+
+ key_chain = self.marionette.actions.sequence("key", "keyboard1")
+ key_chain.send_keys("fooba").pause(100).key_down("r").perform()
+
+This will simulate entering "fooba" into the input field, waiting for 100ms,
+and pressing the key "r". The pause is optional in this case, but can be useful
+for simulating delays typical to a users behaviour.
diff --git a/testing/marionette/client/docs/advanced/debug.rst b/testing/marionette/client/docs/advanced/debug.rst
new file mode 100644
index 0000000000..895009ef7f
--- /dev/null
+++ b/testing/marionette/client/docs/advanced/debug.rst
@@ -0,0 +1,35 @@
+Debugging
+=========
+
+.. py:currentmodule:: marionette_driver.marionette
+
+Sometimes when working with Marionette you'll run into unexpected behaviour and
+need to do some debugging. This page outlines some of the Marionette methods
+that can be useful to you.
+
+Please note that the best tools for debugging are the `ones that ship with
+Gecko`_. This page doesn't describe how to use those with Marionette. Also see
+a related topic about `using the debugger with Marionette`_ on MDN.
+
+.. _ones that ship with Gecko: https://developer.mozilla.org/en-US/docs/Tools
+.. _using the debugger with Marionette: https://developer.mozilla.org/en-US/docs/Marionette/Debugging
+
+Seeing What's on the Page
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes it's difficult to tell what is actually on the page that is being
+manipulated. Either because it happens too fast, the window isn't big enough or
+you are manipulating a remote server! There are two methods that can help you
+out. The first is :func:`~Marionette.screenshot`::
+
+ marionette.screenshot() # takes screenshot of entire frame
+ elem = marionette.find_element(By.ID, 'some-div')
+ marionette.screenshot(elem) # takes a screenshot of only the given element
+
+Sometimes you just want to see the DOM layout. You can do this with the
+:attr:`~Marionette.page_source` property. Note that the page source depends on
+the context you are in::
+
+ print(marionette.page_source)
+ marionette.set_context('chrome')
+ print(marionette.page_source)
diff --git a/testing/marionette/client/docs/advanced/findelement.rst b/testing/marionette/client/docs/advanced/findelement.rst
new file mode 100644
index 0000000000..9d4a34b052
--- /dev/null
+++ b/testing/marionette/client/docs/advanced/findelement.rst
@@ -0,0 +1,87 @@
+Finding Elements
+================
+.. py:currentmodule:: marionette_driver.marionette
+
+One of the most common and yet often most difficult tasks in Marionette is
+finding a DOM element on a webpage or in the chrome UI. Marionette provides
+several different search strategies to use when finding elements. All search
+strategies work with both :func:`~Marionette.find_element` and
+:func:`~Marionette.find_elements`, though some strategies are not implemented
+in chrome scope.
+
+In the event that more than one element is matched by the query,
+:func:`~Marionette.find_element` will only return the first element found. In
+the event that no elements are matched by the query,
+:func:`~Marionette.find_element` will raise `NoSuchElementException` while
+:func:`~Marionette.find_elements` will return an empty list.
+
+Search Strategies
+-----------------
+
+Search strategies are defined in the :class:`By` class::
+
+ from marionette_driver import By
+ print(By.ID)
+
+The strategies are:
+
+* `id` - The easiest way to find an element is to refer to its id directly::
+
+ container = client.find_element(By.ID, 'container')
+
+* `class name` - To find elements belonging to a certain class, use `class name`::
+
+ buttons = client.find_elements(By.CLASS_NAME, 'button')
+
+* `css selector` - It's also possible to find elements using a `css selector`_::
+
+ container_buttons = client.find_elements(By.CSS_SELECTOR, '#container .buttons')
+
+* `name` - Find elements by their name attribute (not implemented in chrome
+ scope)::
+
+ form = client.find_element(By.NAME, 'signup')
+
+* `tag name` - To find all the elements with a given tag, use `tag name`::
+
+ paragraphs = client.find_elements(By.TAG_NAME, 'p')
+
+* `link text` - A convenience strategy for finding link elements by their
+ innerHTML (not implemented in chrome scope)::
+
+ link = client.find_element(By.LINK_TEXT, 'Click me!')
+
+* `partial link text` - Same as `link text` except substrings of the innerHTML
+ are matched (not implemented in chrome scope)::
+
+ link = client.find_element(By.PARTIAL_LINK_TEXT, 'Clic')
+
+* `xpath` - Find elements using an xpath_ query::
+
+ elem = client.find_element(By.XPATH, './/*[@id="foobar"')
+
+.. _css selector: https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors
+.. _xpath: https://developer.mozilla.org/en-US/docs/Web/XPath
+
+
+
+Chaining Searches
+-----------------
+
+In addition to the methods on the Marionette object, WebElement objects also
+provide :func:`~WebElement.find_element` and :func:`~WebElement.find_elements`
+methods. The difference is that only child nodes of the element will be searched.
+Consider the following html snippet::
+
+ <div id="content">
+ <span id="main"></span>
+ </div>
+ <div id="footer"></div>
+
+Doing the following will work::
+
+ client.find_element(By.ID, 'container').find_element(By.ID, 'main')
+
+But this will raise a `NoSuchElementException`::
+
+ client.find_element(By.ID, 'container').find_element(By.ID, 'footer')
diff --git a/testing/marionette/client/docs/advanced/landing.rst b/testing/marionette/client/docs/advanced/landing.rst
new file mode 100644
index 0000000000..0a44de63d7
--- /dev/null
+++ b/testing/marionette/client/docs/advanced/landing.rst
@@ -0,0 +1,13 @@
+Advanced Topics
+===============
+
+Here are a collection of articles explaining some of the more complicated
+aspects of Marionette.
+
+.. toctree::
+ :maxdepth: 1
+
+ findelement
+ stale
+ actions
+ debug
diff --git a/testing/marionette/client/docs/advanced/stale.rst b/testing/marionette/client/docs/advanced/stale.rst
new file mode 100644
index 0000000000..f90ab63579
--- /dev/null
+++ b/testing/marionette/client/docs/advanced/stale.rst
@@ -0,0 +1,76 @@
+Dealing with Stale Elements
+===========================
+.. py:currentmodule:: marionette_driver.marionette
+
+Marionette does not keep a live representation of the DOM saved. All it can do
+is send commands to the Marionette server which queries the DOM on the client's
+behalf. References to elements are also not passed from server to client. A
+unique id is generated for each element that gets referenced and a mapping of
+id to element object is stored on the server. When commands such as
+:func:`~WebElement.click` are run, the client sends the element's id along
+with the command. The server looks up the proper DOM element in its reference
+table and executes the command on it.
+
+In practice this means that the DOM can change state and Marionette will never
+know until it sends another query. For example, look at the following HTML::
+
+ <head>
+ <script type=text/javascript>
+ function addDiv() {
+ var div = document.createElement("div");
+ document.getElementById("container").appendChild(div);
+ }
+ </script>
+ </head>
+
+ <body>
+ <div id="container">
+ </div>
+ <input id="button" type=button onclick="addDiv();">
+ </body>
+
+Care needs to be taken as the DOM is being modified after the page has loaded.
+The following code has a race condition::
+
+ button = client.find_element('id', 'button')
+ button.click()
+ assert len(client.find_elements('css selector', '#container div')) > 0
+
+
+Explicit Waiting and Expected Conditions
+----------------------------------------
+.. py:currentmodule:: marionette_driver
+
+To avoid the above scenario, manual synchronisation is needed. Waits are used
+to pause program execution until a given condition is true. This is a useful
+technique to employ when documents load new content or change after
+``Document.readyState``'s value changes to "complete".
+
+The :class:`Wait` helper class provided by Marionette avoids some of the
+caveats of ``time.sleep(n)``. It will return immediately once the provided
+condition evaluates to true.
+
+To avoid the race condition in the above example, one could do::
+
+ from marionette_driver import Wait
+
+ button = client.find_element('id', 'button')
+ button.click()
+
+ def find_divs():
+ return client.find_elements('css selector', '#container div')
+
+ divs = Wait(client).until(find_divs)
+ assert len(divs) > 0
+
+This avoids the race condition. Because finding elements is a common condition
+to wait for, it is built in to Marionette. Instead of the above, you could
+write::
+
+ from marionette_driver import Wait
+
+ button = client.find_element('id', 'button')
+ button.click()
+ assert len(Wait(client).until(expected.elements_present('css selector', '#container div'))) > 0
+
+For a full list of built-in conditions, see :mod:`~marionette_driver.expected`.
diff --git a/testing/marionette/client/docs/basics.rst b/testing/marionette/client/docs/basics.rst
new file mode 100644
index 0000000000..76ae71015b
--- /dev/null
+++ b/testing/marionette/client/docs/basics.rst
@@ -0,0 +1,195 @@
+.. py:currentmodule:: marionette_driver.marionette
+
+Marionette Python Client
+========================
+
+The Marionette Python client library allows you to remotely control a
+Gecko-based browser or device which is running a Marionette_
+server. This includes Firefox Desktop and Firefox for Android.
+
+The Marionette server is built directly into Gecko and can be started by
+passing in a command line option to Gecko, or by using a Marionette-enabled
+build. The server listens for connections from various clients. Clients can
+then control Gecko by sending commands to the server.
+
+This is the official Python client for Marionette. There also exists a
+`NodeJS client`_ maintained by the Firefox OS automation team.
+
+.. _Marionette: https://developer.mozilla.org/en-US/docs/Marionette
+.. _NodeJS client: https://github.com/mozilla-b2g/gaia/tree/master/tests/jsmarionette
+
+Getting the Client
+------------------
+
+The Python client is officially supported. To install it, first make sure you
+have `pip installed`_ then run:
+
+.. code-block:: bash
+
+ $ pip install marionette_driver
+
+It's highly recommended to use virtualenv_ when installing Marionette to avoid
+package conflicts and other general nastiness.
+
+You should now be ready to start using Marionette. The best way to learn is to
+play around with it. Start a `Marionette-enabled instance of Firefox`_, fire up
+a python shell and follow along with the
+:doc:`interactive tutorial <interactive>`!
+
+.. _pip installed: https://pip.pypa.io/en/latest/installing.html
+.. _virtualenv: http://virtualenv.readthedocs.org/en/latest/
+.. _Marionette-enabled instance of Firefox: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Builds
+
+Using the Client for Testing
+----------------------------
+
+Please visit the `Marionette Tests`_ section on MDN for information regarding
+testing with Marionette.
+
+.. _Marionette Tests: https://developer.mozilla.org/en/Marionette/Tests
+
+Session Management
+------------------
+A session is a single instance of a Marionette client connected to a Marionette
+server. Before you can start executing commands, you need to start a session
+with :func:`start_session() <Marionette.start_session>`:
+
+.. code-block:: python
+
+ from marionette_driver.marionette import Marionette
+
+ client = Marionette('127.0.0.1', port=2828)
+ client.start_session()
+
+This returns a session id and an object listing the capabilities of the
+Marionette server. For example, a server running on Firefox Desktop will
+have some features which a server running from Firefox Android won't.
+It's also possible to access the capabilities using the
+:attr:`~Marionette.session_capabilities` attribute. After finishing with a
+session, you can delete it with :func:`~Marionette.delete_session()`. Note that
+this will also happen automatically when the Marionette object is garbage
+collected.
+
+Context Management
+------------------
+Commands can only be executed in a single window, frame and scope at a time. In
+order to run commands elsewhere, it's necessary to explicitly switch to the
+appropriate context.
+
+Use :func:`~Marionette.switch_to_window` to execute commands in the context of a
+new window:
+
+.. code-block:: python
+
+ original_window = client.current_window_handle
+ for handle in client.window_handles:
+ if handle != original_window:
+ client.switch_to_window(handle)
+ print("Switched to window with '{}' loaded.".format(client.get_url()))
+ client.switch_to_window(original_window)
+
+Similarly, use :func:`~Marionette.switch_to_frame` to execute commands in the
+context of a new frame (e.g an <iframe> element):
+
+.. code-block:: python
+
+ iframe = client.find_element(By.TAG_NAME, 'iframe')
+ client.switch_to_frame(iframe)
+
+Finally Marionette can switch between `chrome` and `content` scope. Chrome is a
+privileged scope where you can access things like the Firefox UI itself.
+Content scope is where things like webpages live. You can switch between
+`chrome` and `content` using the :func:`~Marionette.set_context` and :func:`~Marionette.using_context` functions:
+
+.. code-block:: python
+
+ client.set_context(client.CONTEXT_CONTENT)
+ # content scope
+ with client.using_context(client.CONTEXT_CHROME):
+ #chrome scope
+ pass # ... do stuff ...
+ # content scope restored
+
+
+Navigation
+----------
+
+Use :func:`~Marionette.navigate` to open a new website. It's also possible to
+move through the back/forward cache using :func:`~Marionette.go_forward` and
+:func:`~Marionette.go_back` respectively. To retrieve the currently
+open website, use :func:`~Marionette.get_url`:
+
+.. code-block:: python
+
+ url = 'http://mozilla.org'
+ client.navigate(url)
+ client.go_back()
+ client.go_forward()
+ assert client.get_url() == url
+
+
+DOM Elements
+------------
+
+In order to inspect or manipulate actual DOM elements, they must first be found
+using the :func:`~Marionette.find_element` or :func:`~Marionette.find_elements`
+methods:
+
+.. code-block:: python
+
+ from marionette_driver.marionette import WebElement
+ element = client.find_element(By.ID, 'my-id')
+ assert type(element) == WebElement
+ elements = client.find_elements(By.TAG_NAME, 'a')
+ assert type(elements) == list
+
+For a full list of valid search strategies, see :doc:`advanced/findelement`.
+
+Now that an element has been found, it's possible to manipulate it:
+
+.. code-block:: python
+
+ element.click()
+ element.send_keys('hello!')
+ print(element.get_attribute('style'))
+
+For the full list of possible commands, see the :class:`WebElement`
+reference.
+
+Be warned that a reference to an element object can become stale if it was
+modified or removed from the document. See :doc:`advanced/stale` for tips
+on working around this limitation.
+
+Script Execution
+----------------
+
+Sometimes Marionette's provided APIs just aren't enough and it is necessary to
+run arbitrary javascript. This is accomplished with the
+:func:`~Marionette.execute_script` and :func:`~Marionette.execute_async_script`
+functions. They accomplish what their names suggest, the former executes some
+synchronous JavaScript, while the latter provides a callback mechanism for
+running asynchronous JavaScript:
+
+.. code-block:: python
+
+ result = client.execute_script("return arguments[0] + arguments[1];",
+ script_args=[2, 3])
+ assert result == 5
+
+The async method works the same way, except it won't return until the
+`resolve()` function is called:
+
+.. code-block:: python
+
+ result = client.execute_async_script("""
+ let [resolve] = arguments;
+ setTimeout(function() {
+ resolve("all done");
+ }, arguments[0]);
+ """, script_args=[1000])
+ assert result == "all done"
+
+Beware that running asynchronous scripts can potentially hang the program
+indefinitely if they are not written properly. It is generally a good idea to
+set a script timeout using :func:`~Marionette.timeout.script` and handling
+`ScriptTimeoutException`.
diff --git a/testing/marionette/client/docs/conf.py b/testing/marionette/client/docs/conf.py
new file mode 100644
index 0000000000..692545faa9
--- /dev/null
+++ b/testing/marionette/client/docs/conf.py
@@ -0,0 +1,274 @@
+# -*- coding: utf-8 -*-
+#
+# Marionette Python Client documentation build configuration file, created by
+# sphinx-quickstart on Tue Aug 6 13:54:46 2013.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import os
+import sys
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+# sys.path.insert(0, os.path.abspath('.'))
+
+here = os.path.dirname(os.path.abspath(__file__))
+parent = os.path.dirname(here)
+sys.path.insert(0, parent)
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ["sphinx.ext.autodoc"]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix of source filenames.
+source_suffix = ".rst"
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+project = "Marionette Python Client"
+copyright = "2013, Mozilla Automation and Tools and individual contributors"
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+# version = '0'
+# The full version, including alpha/beta/rc tags.
+# release = '0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ''
+# Else, today_fmt is used as the format for a strftime call.
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ["_build"]
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+
+html_theme = "default"
+
+on_rtd = os.environ.get("READTHEDOCS", None) == "True"
+
+if not on_rtd:
+ try:
+ import sphinx_rtd_theme
+
+ html_theme = "sphinx_rtd_theme"
+ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+ except ImportError:
+ pass
+
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+# html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+# html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+# html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+# html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+# html_domain_indices = True
+
+# If false, no index is generated.
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+html_show_copyright = False
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "MarionettePythonClientdoc"
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ # 'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ # 'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ # 'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ (
+ "index",
+ "MarionettePythonClient.tex",
+ "Marionette Python Client Documentation",
+ "Mozilla Automation and Tools team",
+ "manual",
+ ),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+# latex_appendices = []
+
+# If false, no module index is generated.
+# latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (
+ "index",
+ "marionettepythonclient",
+ "Marionette Python Client Documentation",
+ ["Mozilla Automation and Tools team"],
+ 1,
+ )
+]
+
+# If true, show URL addresses after external links.
+# man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ "index",
+ "MarionettePythonClient",
+ "Marionette Python Client Documentation",
+ "Mozilla Automation and Tools team",
+ "MarionettePythonClient",
+ "One line description of project.",
+ "Miscellaneous",
+ ),
+]
+
+# Documents to append as an appendix to all manuals.
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+# texinfo_show_urls = 'footnote'
diff --git a/testing/marionette/client/docs/index.rst b/testing/marionette/client/docs/index.rst
new file mode 100644
index 0000000000..b1f266726c
--- /dev/null
+++ b/testing/marionette/client/docs/index.rst
@@ -0,0 +1,16 @@
+.. include:: basics.rst
+
+Indices and tables
+------------------
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+.. toctree::
+ :hidden:
+
+ Getting Started <basics>
+ Interactive Tutorial <interactive>
+ advanced/landing
+ reference
diff --git a/testing/marionette/client/docs/interactive.rst b/testing/marionette/client/docs/interactive.rst
new file mode 100644
index 0000000000..7b2ebe2ec3
--- /dev/null
+++ b/testing/marionette/client/docs/interactive.rst
@@ -0,0 +1,52 @@
+Using the Client Interactively
+==============================
+
+Once you installed the client and have Marionette running, you can fire
+up your favourite interactive python environment and start playing with
+Marionette. Let's use a typical python shell:
+
+.. parsed-literal::
+ python
+
+First, import Marionette:
+
+.. parsed-literal::
+ from marionette_driver.marionette import Marionette
+
+Now create the client for this session. Assuming you're using the default
+port on a Marionette instance running locally:
+
+.. parsed-literal::
+ client = Marionette(host='127.0.0.1', port=2828)
+ client.start_session()
+
+This will return some id representing your session id. Now that you've
+established a connection, let's start doing interesting things:
+
+.. parsed-literal::
+ client.navigate("http://www.mozilla.org")
+
+Now you're at mozilla.org! You can even verify it using the following:
+
+.. parsed-literal::
+ client.get_url()
+
+You can execute Javascript code in the scope of the web page:
+
+.. parsed-literal::
+ client.execute_script("return window.document.title;")
+
+This will you return the title of the web page as set in the head section
+of the HTML document.
+
+Also you can find elements and click on those. Let's say you want to get
+the first link:
+
+.. parsed-literal::
+ from marionette_driver import By
+ first_link = client.find_element(By.TAG_NAME, "a")
+
+first_link now holds a reference to the first link on the page. You can click it:
+
+.. parsed-literal::
+ first_link.click()
diff --git a/testing/marionette/client/docs/make.bat b/testing/marionette/client/docs/make.bat
new file mode 100644
index 0000000000..fb02fc1a8c
--- /dev/null
+++ b/testing/marionette/client/docs/make.bat
@@ -0,0 +1,190 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\MarionettePythonClient.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\MarionettePythonClient.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
diff --git a/testing/marionette/client/docs/reference.rst b/testing/marionette/client/docs/reference.rst
new file mode 100644
index 0000000000..ed4a7ce109
--- /dev/null
+++ b/testing/marionette/client/docs/reference.rst
@@ -0,0 +1,66 @@
+=============
+API Reference
+=============
+
+Marionette
+----------
+.. py:currentmodule:: marionette_driver.marionette.Marionette
+.. autoclass:: marionette_driver.marionette.Marionette
+ :members:
+
+WebElement
+-----------
+.. py:currentmodule:: marionette_driver.marionette.WebElement
+.. autoclass:: marionette_driver.marionette.WebElement
+ :members:
+
+DateTimeValue
+-------------
+.. py:currentmodule:: marionette_driver.DateTimeValue
+.. autoclass:: marionette_driver.DateTimeValue
+ :members:
+
+Actions
+-------
+.. py:currentmodule:: marionette_driver.marionette.Actions
+.. autoclass:: marionette_driver.marionette.Actions
+ :members:
+
+Alert
+-----
+.. py:currentmodule:: marionette_driver.marionette.Alert
+.. autoclass:: marionette_driver.marionette.Alert
+ :members:
+
+Wait
+----
+.. py:currentmodule:: marionette_driver.Wait
+.. autoclass:: marionette_driver.Wait
+ :members:
+ :special-members:
+.. autoattribute marionette_driver.wait.DEFAULT_TIMEOUT
+.. autoattribute marionette_driver.wait.DEFAULT_INTERVAL
+
+Built-in Conditions
+^^^^^^^^^^^^^^^^^^^
+.. py:currentmodule:: marionette_driver.expected
+.. automodule:: marionette_driver.expected
+ :members:
+
+Timeouts
+--------
+.. py:currentmodule:: marionette_driver.timeout.Timeouts
+.. autoclass:: marionette_driver.timeout.Timeouts
+ :members:
+
+Addons
+------
+.. py:currentmodule:: marionette_driver.addons.Addons
+.. autoclass:: marionette_driver.addons.Addons
+ :members:
+
+Localization
+------------
+.. py:currentmodule:: marionette_driver.localization.L10n
+.. autoclass:: marionette_driver.localization.L10n
+ :members:
diff --git a/testing/marionette/client/marionette_driver/__init__.py b/testing/marionette/client/marionette_driver/__init__.py
new file mode 100644
index 0000000000..443fbd8fc3
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/__init__.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/.
+
+__version__ = "3.4.0"
+
+from marionette_driver import (
+ addons,
+ by,
+ date_time_value,
+ decorators,
+ errors,
+ expected,
+ geckoinstance,
+ keys,
+ localization,
+ marionette,
+ wait,
+)
+from marionette_driver.by import By
+from marionette_driver.date_time_value import DateTimeValue
+from marionette_driver.wait import Wait
diff --git a/testing/marionette/client/marionette_driver/addons.py b/testing/marionette/client/marionette_driver/addons.py
new file mode 100644
index 0000000000..09f44e3e54
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/addons.py
@@ -0,0 +1,76 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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
+
+from . import errors
+
+__all__ = ["Addons", "AddonInstallException"]
+
+
+class AddonInstallException(errors.MarionetteException):
+ pass
+
+
+class Addons(object):
+ """An API for installing and inspecting addons during Gecko
+ runtime. This is a partially implemented wrapper around Gecko's
+ `AddonManager API`_.
+
+ For example::
+
+ from marionette_driver.addons import Addons
+ addons = Addons(marionette)
+ addons.install("/path/to/extension.xpi")
+
+ .. _AddonManager API: https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager
+
+ """
+
+ def __init__(self, marionette):
+ self._mn = marionette
+
+ def install(self, path, temp=False):
+ """Install a Firefox addon.
+
+ If the addon is restartless, it can be used right away. Otherwise
+ a restart using :func:`~marionette_driver.marionette.Marionette.restart`
+ will be needed.
+
+ :param path: A file path to the extension to be installed.
+ :param temp: Install a temporary addon. Temporary addons will
+ automatically be uninstalled on shutdown and do not need
+ to be signed, though they must be restartless.
+
+ :returns: The addon ID string of the newly installed addon.
+
+ :raises: :exc:`AddonInstallException`
+
+ """
+ # On windows we can end up with a path with mixed \ and /
+ # which Firefox doesn't like
+ path = path.replace("/", os.path.sep)
+
+ body = {"path": path, "temporary": temp}
+ try:
+ return self._mn._send_message("Addon:Install", body, key="value")
+ except errors.UnknownException as e:
+ raise AddonInstallException(e)
+
+ def uninstall(self, addon_id):
+ """Uninstall a Firefox addon.
+
+ If the addon is restartless, it will be uninstalled right away.
+ Otherwise a restart using :func:`~marionette_driver.marionette.Marionette.restart`
+ will be needed.
+
+ If the call to uninstall is resulting in a `ScriptTimeoutException`,
+ an invalid ID is likely being passed in. Unfortunately due to
+ AddonManager's implementation, it's hard to retrieve this error from
+ Python.
+
+ :param addon_id: The addon ID string to uninstall.
+
+ """
+ self._mn._send_message("Addon:Uninstall", {"id": addon_id})
diff --git a/testing/marionette/client/marionette_driver/by.py b/testing/marionette/client/marionette_driver/by.py
new file mode 100644
index 0000000000..b54ca729f2
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/by.py
@@ -0,0 +1,25 @@
+# Copyright 2008-2009 WebDriver committers
+# Copyright 2008-2009 Google Inc.
+#
+# 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.
+
+
+class By(object):
+ ID = "id"
+ XPATH = "xpath"
+ LINK_TEXT = "link text"
+ PARTIAL_LINK_TEXT = "partial link text"
+ NAME = "name"
+ TAG_NAME = "tag name"
+ CLASS_NAME = "class name"
+ CSS_SELECTOR = "css selector"
diff --git a/testing/marionette/client/marionette_driver/date_time_value.py b/testing/marionette/client/marionette_driver/date_time_value.py
new file mode 100644
index 0000000000..c6f2ed989a
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/date_time_value.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/.
+
+
+class DateTimeValue(object):
+ """
+ Interface for setting the value of HTML5 "date" and "time" input elements.
+
+ Simple usage example:
+
+ ::
+
+ element = marionette.find_element(By.ID, "date-test")
+ dt_value = DateTimeValue(element)
+ dt_value.date = datetime(1998, 6, 2)
+
+ """
+
+ def __init__(self, element):
+ self.element = element
+
+ @property
+ def date(self):
+ """
+ Retrieve the element's string value
+ """
+ return self.element.get_attribute("value")
+
+ # As per the W3C "date" element specification
+ # (http://dev.w3.org/html5/markup/input.date.html), this value is formatted
+ # according to RFC 3339: http://tools.ietf.org/html/rfc3339#section-5.6
+ @date.setter
+ def date(self, date_value):
+ self.element.send_keys(date_value.strftime("%Y-%m-%d"))
+
+ @property
+ def time(self):
+ """
+ Retrieve the element's string value
+ """
+ return self.element.get_attribute("value")
+
+ # As per the W3C "time" element specification
+ # (http://dev.w3.org/html5/markup/input.time.html), this value is formatted
+ # according to RFC 3339: http://tools.ietf.org/html/rfc3339#section-5.6
+ @time.setter
+ def time(self, time_value):
+ self.element.send_keys(time_value.strftime("%H:%M:%S"))
diff --git a/testing/marionette/client/marionette_driver/decorators.py b/testing/marionette/client/marionette_driver/decorators.py
new file mode 100644
index 0000000000..95a5c5bbee
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/decorators.py
@@ -0,0 +1,79 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 socket
+from functools import wraps
+
+
+def _find_marionette_in_args(*args, **kwargs):
+ try:
+ m = [a for a in args + tuple(kwargs.values()) if hasattr(a, "session")][0]
+ except IndexError:
+ print("Can only apply decorator to function using a marionette object")
+ raise
+ return m
+
+
+def do_process_check(func):
+ """Decorator which checks the process status after the function has run."""
+
+ @wraps(func)
+ def _(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except (socket.error, socket.timeout):
+ m = _find_marionette_in_args(*args, **kwargs)
+
+ # In case of socket failures which will also include crashes of the
+ # application, make sure to handle those correctly. In case of an
+ # active shutdown just let it bubble up.
+ if m.is_shutting_down:
+ raise
+
+ m._handle_socket_failure()
+
+ return _
+
+
+def uses_marionette(func):
+ """Decorator which creates a marionette session and deletes it
+ afterwards if one doesn't already exist.
+ """
+
+ @wraps(func)
+ def _(*args, **kwargs):
+ m = _find_marionette_in_args(*args, **kwargs)
+ delete_session = False
+ if not m.session:
+ delete_session = True
+ m.start_session()
+
+ m.set_context(m.CONTEXT_CHROME)
+ ret = func(*args, **kwargs)
+
+ if delete_session:
+ m.delete_session()
+
+ return ret
+
+ return _
+
+
+def using_context(context):
+ """Decorator which allows a function to execute in certain scope
+ using marionette.using_context functionality and returns to old
+ scope once the function exits.
+ :param context: Either 'chrome' or 'content'.
+ """
+
+ def wrap(func):
+ @wraps(func)
+ def inner(*args, **kwargs):
+ m = _find_marionette_in_args(*args, **kwargs)
+ with m.using_context(context):
+ return func(*args, **kwargs)
+
+ return inner
+
+ return wrap
diff --git a/testing/marionette/client/marionette_driver/errors.py b/testing/marionette/client/marionette_driver/errors.py
new file mode 100644
index 0000000000..27e1928a73
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/errors.py
@@ -0,0 +1,206 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 traceback
+
+import six
+
+
+@six.python_2_unicode_compatible
+class MarionetteException(Exception):
+
+ """Raised when a generic non-recoverable exception has occured."""
+
+ status = "webdriver error"
+
+ def __init__(self, message=None, cause=None, stacktrace=None):
+ """Construct new MarionetteException instance.
+
+ :param message: An optional exception message.
+
+ :param cause: An optional tuple of three values giving
+ information about the root exception cause. Expected
+ tuple values are (type, value, traceback).
+
+ :param stacktrace: Optional string containing a stacktrace
+ (typically from a failed JavaScript execution) that will
+ be displayed in the exception's string representation.
+
+ """
+ self.cause = cause
+ self.stacktrace = stacktrace
+ self._message = six.text_type(message)
+
+ def __str__(self):
+ # pylint: disable=W1645
+ msg = self.message
+ tb = None
+
+ if self.cause:
+ if type(self.cause) is tuple:
+ msg += ", caused by {0!r}".format(self.cause[0])
+ tb = self.cause[2]
+ else:
+ msg += ", caused by {}".format(self.cause)
+
+ if self.stacktrace:
+ st = "".join(["\t{}\n".format(x) for x in self.stacktrace.splitlines()])
+ msg += "\nstacktrace:\n{}".format(st)
+
+ if tb:
+ msg += ": " + "".join(traceback.format_tb(tb))
+
+ return six.text_type(msg)
+
+ @property
+ def message(self):
+ return self._message
+
+
+class DetachedShadowRootException(MarionetteException):
+ status = "detached shadow root"
+
+
+class ElementNotSelectableException(MarionetteException):
+ status = "element not selectable"
+
+
+class ElementClickInterceptedException(MarionetteException):
+ status = "element click intercepted"
+
+
+class InsecureCertificateException(MarionetteException):
+ status = "insecure certificate"
+
+
+class InvalidArgumentException(MarionetteException):
+ status = "invalid argument"
+
+
+class InvalidSessionIdException(MarionetteException):
+ status = "invalid session id"
+
+
+class TimeoutException(MarionetteException):
+ status = "timeout"
+
+
+class JavascriptException(MarionetteException):
+ status = "javascript error"
+
+
+class NoSuchElementException(MarionetteException):
+ status = "no such element"
+
+
+class NoSuchShadowRootException(MarionetteException):
+ status = "no such shadow root"
+
+
+class NoSuchWindowException(MarionetteException):
+ status = "no such window"
+
+
+class StaleElementException(MarionetteException):
+ status = "stale element reference"
+
+
+class ScriptTimeoutException(MarionetteException):
+ status = "script timeout"
+
+
+class ElementNotVisibleException(MarionetteException):
+ """Deprecated. Will be removed with the release of Firefox 54."""
+
+ status = "element not visible"
+
+ def __init__(
+ self,
+ message="Element is not currently visible and may not be manipulated",
+ stacktrace=None,
+ cause=None,
+ ):
+ super(ElementNotVisibleException, self).__init__(
+ message, cause=cause, stacktrace=stacktrace
+ )
+
+
+class ElementNotAccessibleException(MarionetteException):
+ status = "element not accessible"
+
+
+class ElementNotInteractableException(MarionetteException):
+ status = "element not interactable"
+
+
+class NoSuchFrameException(MarionetteException):
+ status = "no such frame"
+
+
+class InvalidElementStateException(MarionetteException):
+ status = "invalid element state"
+
+
+class NoAlertPresentException(MarionetteException):
+ status = "no such alert"
+
+
+class InvalidCookieDomainException(MarionetteException):
+ status = "invalid cookie domain"
+
+
+class UnableToSetCookieException(MarionetteException):
+ status = "unable to set cookie"
+
+
+class InvalidElementCoordinates(MarionetteException):
+ status = "invalid element coordinates"
+
+
+class InvalidSelectorException(MarionetteException):
+ status = "invalid selector"
+
+
+class MoveTargetOutOfBoundsException(MarionetteException):
+ status = "move target out of bounds"
+
+
+class SessionNotCreatedException(MarionetteException):
+ status = "session not created"
+
+
+class UnexpectedAlertOpen(MarionetteException):
+ status = "unexpected alert open"
+
+
+class UnknownCommandException(MarionetteException):
+ status = "unknown command"
+
+
+class UnknownException(MarionetteException):
+ status = "unknown error"
+
+
+class UnsupportedOperationException(MarionetteException):
+ status = "unsupported operation"
+
+
+class UnresponsiveInstanceException(Exception):
+ pass
+
+
+es_ = [
+ e
+ for e in locals().values()
+ if type(e) == type and issubclass(e, MarionetteException)
+]
+by_string = {e.status: e for e in es_}
+
+
+def lookup(identifier):
+ """Finds error exception class by associated Selenium JSON wire
+ protocol number code, or W3C WebDriver protocol string.
+
+ """
+ return by_string.get(identifier, MarionetteException)
diff --git a/testing/marionette/client/marionette_driver/expected.py b/testing/marionette/client/marionette_driver/expected.py
new file mode 100644
index 0000000000..37c415686c
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/expected.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/.
+
+import types
+
+from . import errors
+from .marionette import WebElement
+
+"""This file provides a set of expected conditions for common use
+cases when writing Marionette tests.
+
+The conditions rely on explicit waits that retries conditions a number
+of times until they are either successfully met, or they time out.
+
+"""
+
+
+class element_present(object):
+ """Checks that a web element is present in the DOM of the current
+ context. This does not necessarily mean that the element is
+ visible.
+
+ You can select which element to be checked for presence by
+ supplying a locator::
+
+ el = Wait(marionette).until(expected.element_present(By.ID, "foo"))
+
+ Or by using a function/lambda returning an element::
+
+ el = Wait(marionette).until(
+ expected.element_present(lambda m: m.find_element(By.ID, "foo")))
+
+ :param args: locator or function returning web element
+ :returns: the web element once it is located, or False
+
+ """
+
+ def __init__(self, *args):
+ if len(args) == 1 and isinstance(args[0], types.FunctionType):
+ self.locator = args[0]
+ else:
+ self.locator = lambda m: m.find_element(*args)
+
+ def __call__(self, marionette):
+ return _find(marionette, self.locator)
+
+
+class element_not_present(element_present):
+ """Checks that a web element is not present in the DOM of the current
+ context.
+
+ You can select which element to be checked for lack of presence by
+ supplying a locator::
+
+ r = Wait(marionette).until(expected.element_not_present(By.ID, "foo"))
+
+ Or by using a function/lambda returning an element::
+
+ r = Wait(marionette).until(
+ expected.element_present(lambda m: m.find_element(By.ID, "foo")))
+
+ :param args: locator or function returning web element
+ :returns: True if element is not present, or False if it is present
+
+ """
+
+ def __init__(self, *args):
+ super(element_not_present, self).__init__(*args)
+
+ def __call__(self, marionette):
+ return not super(element_not_present, self).__call__(marionette)
+
+
+class element_stale(object):
+ """Check that the given element is no longer attached to DOM of the
+ current context.
+
+ This can be useful for waiting until an element is no longer
+ present.
+
+ Sample usage::
+
+ el = marionette.find_element(By.ID, "foo")
+ # ...
+ Wait(marionette).until(expected.element_stale(el))
+
+ :param element: the element to wait for
+ :returns: False if the element is still attached to the DOM, True
+ otherwise
+
+ """
+
+ def __init__(self, element):
+ self.el = element
+
+ def __call__(self, marionette):
+ try:
+ # Calling any method forces a staleness check
+ self.el.is_enabled()
+ return False
+ except (errors.StaleElementException, errors.NoSuchElementException):
+ # StaleElementException is raised when the element is gone, and
+ # NoSuchElementException is raised after a process swap.
+ return True
+
+
+class elements_present(object):
+ """Checks that web elements are present in the DOM of the current
+ context. This does not necessarily mean that the elements are
+ visible.
+
+ You can select which elements to be checked for presence by
+ supplying a locator::
+
+ els = Wait(marionette).until(expected.elements_present(By.TAG_NAME, "a"))
+
+ Or by using a function/lambda returning a list of elements::
+
+ els = Wait(marionette).until(
+ expected.elements_present(lambda m: m.find_elements(By.TAG_NAME, "a")))
+
+ :param args: locator or function returning a list of web elements
+ :returns: list of web elements once they are located, or False
+
+ """
+
+ def __init__(self, *args):
+ if len(args) == 1 and isinstance(args[0], types.FunctionType):
+ self.locator = args[0]
+ else:
+ self.locator = lambda m: m.find_elements(*args)
+
+ def __call__(self, marionette):
+ return _find(marionette, self.locator)
+
+
+class elements_not_present(elements_present):
+ """Checks that web elements are not present in the DOM of the
+ current context.
+
+ You can select which elements to be checked for not being present
+ by supplying a locator::
+
+ r = Wait(marionette).until(expected.elements_not_present(By.TAG_NAME, "a"))
+
+ Or by using a function/lambda returning a list of elements::
+
+ r = Wait(marionette).until(
+ expected.elements_not_present(lambda m: m.find_elements(By.TAG_NAME, "a")))
+
+ :param args: locator or function returning a list of web elements
+ :returns: True if elements are missing, False if one or more are
+ present
+
+ """
+
+ def __init__(self, *args):
+ super(elements_not_present, self).__init__(*args)
+
+ def __call__(self, marionette):
+ return not super(elements_not_present, self).__call__(marionette)
+
+
+class element_displayed(object):
+ """An expectation for checking that an element is visible.
+
+ Visibility means that the element is not only displayed, but also
+ has a height and width that is greater than 0 pixels.
+
+ Stale elements, meaning elements that have been detached from the
+ DOM of the current context are treated as not being displayed,
+ meaning this expectation is not analogous to the behaviour of
+ calling :func:`~marionette_driver.marionette.WebElement.is_displayed`
+ on an :class:`~marionette_driver.marionette.WebElement`.
+
+ You can select which element to be checked for visibility by
+ supplying a locator::
+
+ displayed = Wait(marionette).until(expected.element_displayed(By.ID, "foo"))
+
+ Or by supplying an element::
+
+ el = marionette.find_element(By.ID, "foo")
+ displayed = Wait(marionette).until(expected.element_displayed(el))
+
+ :param args: locator or web element
+ :returns: True if element is displayed, False if hidden
+
+ """
+
+ def __init__(self, *args):
+ self.el = None
+ if len(args) == 1 and isinstance(args[0], WebElement):
+ self.el = args[0]
+ else:
+ self.locator = lambda m: m.find_element(*args)
+
+ def __call__(self, marionette):
+ if self.el is None:
+ self.el = _find(marionette, self.locator)
+ if not self.el:
+ return False
+ try:
+ return self.el.is_displayed()
+ except errors.StaleElementException:
+ return False
+
+
+class element_not_displayed(element_displayed):
+ """An expectation for checking that an element is not visible.
+
+ Visibility means that the element is not only displayed, but also
+ has a height and width that is greater than 0 pixels.
+
+ Stale elements, meaning elements that have been detached fom the
+ DOM of the current context are treated as not being displayed,
+ meaning this expectation is not analogous to the behaviour of
+ calling :func:`~marionette_driver.marionette.WebElement.is_displayed`
+ on an :class:`~marionette_driver.marionette.WebElement`.
+
+ You can select which element to be checked for visibility by
+ supplying a locator::
+
+ hidden = Wait(marionette).until(expected.element_not_displayed(By.ID, "foo"))
+
+ Or by supplying an element::
+
+ el = marionette.find_element(By.ID, "foo")
+ hidden = Wait(marionette).until(expected.element_not_displayed(el))
+
+ :param args: locator or web element
+ :returns: True if element is hidden, False if displayed
+
+ """
+
+ def __init__(self, *args):
+ super(element_not_displayed, self).__init__(*args)
+
+ def __call__(self, marionette):
+ return not super(element_not_displayed, self).__call__(marionette)
+
+
+class element_selected(object):
+ """An expectation for checking that the given element is selected.
+
+ :param element: the element to be selected
+ :returns: True if element is selected, False otherwise
+
+ """
+
+ def __init__(self, element):
+ self.el = element
+
+ def __call__(self, marionette):
+ return self.el.is_selected()
+
+
+class element_not_selected(element_selected):
+ """An expectation for checking that the given element is not
+ selected.
+
+ :param element: the element to not be selected
+ :returns: True if element is not selected, False if selected
+
+ """
+
+ def __init__(self, element):
+ super(element_not_selected, self).__init__(element)
+
+ def __call__(self, marionette):
+ return not super(element_not_selected, self).__call__(marionette)
+
+
+class element_enabled(object):
+ """An expectation for checking that the given element is enabled.
+
+ :param element: the element to check if enabled
+ :returns: True if element is enabled, False otherwise
+
+ """
+
+ def __init__(self, element):
+ self.el = element
+
+ def __call__(self, marionette):
+ return self.el.is_enabled()
+
+
+class element_not_enabled(element_enabled):
+ """An expectation for checking that the given element is disabled.
+
+ :param element: the element to check if disabled
+ :returns: True if element is disabled, False if enabled
+
+ """
+
+ def __init__(self, element):
+ super(element_not_enabled, self).__init__(element)
+
+ def __call__(self, marionette):
+ return not super(element_not_enabled, self).__call__(marionette)
+
+
+def _find(marionette, func):
+ el = None
+
+ try:
+ el = func(marionette)
+ except errors.NoSuchElementException:
+ pass
+
+ if el is None:
+ return False
+ return el
diff --git a/testing/marionette/client/marionette_driver/geckoinstance.py b/testing/marionette/client/marionette_driver/geckoinstance.py
new file mode 100644
index 0000000000..b0bec22ea0
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -0,0 +1,663 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/
+
+# ALL CHANGES TO THIS FILE MUST HAVE REVIEW FROM A MARIONETTE PEER!
+#
+# Please refer to INSTRUCTIONS TO ADD A NEW PREFERENCE in
+# remote/shared/RecommendedPreferences.sys.mjs
+#
+# The Marionette Python client is used out-of-tree with various builds of
+# Firefox. Removing a preference from this file will cause regressions,
+# so please be careful and get review from a Testing :: Marionette peer
+# before you make any changes to this file.
+
+import codecs
+import json
+import os
+import sys
+import tempfile
+import time
+import traceback
+from copy import deepcopy
+
+import mozversion
+import six
+from mozprofile import Profile
+from mozrunner import FennecEmulatorRunner, Runner
+from six import reraise
+
+from . import errors
+
+
+class GeckoInstance(object):
+ required_prefs = {
+ # Make sure Shield doesn't hit the network.
+ "app.normandy.api_url": "",
+ # Increase the APZ content response timeout in tests to 1 minute.
+ # This is to accommodate the fact that test environments tends to be slower
+ # than production environments (with the b2g emulator being the slowest of them
+ # all), resulting in the production timeout value sometimes being exceeded
+ # and causing false-positive test failures. See bug 1176798, bug 1177018,
+ # bug 1210465.
+ "apz.content_response_timeout": 60000,
+ # Don't pull sponsored Top Sites content from the network
+ "browser.newtabpage.activity-stream.showSponsoredTopSites": False,
+ # Disable geolocation ping (#1)
+ "browser.region.network.url": "",
+ # Don't pull Top Sites content from the network
+ "browser.topsites.contile.enabled": False,
+ # Disable UI tour
+ "browser.uitour.enabled": False,
+ # Disable captive portal
+ "captivedetect.canonicalURL": "",
+ # Defensively disable data reporting systems
+ "datareporting.healthreport.documentServerURI": "http://%(server)s/dummy/healthreport/",
+ "datareporting.healthreport.logging.consoleEnabled": False,
+ "datareporting.healthreport.service.enabled": False,
+ "datareporting.healthreport.service.firstRun": False,
+ "datareporting.healthreport.uploadEnabled": False,
+ # Do not show datareporting policy notifications which can interfere with tests
+ "datareporting.policy.dataSubmissionEnabled": False,
+ "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
+ # Enabling the support for File object creation in the content process.
+ "dom.file.createInChild": True,
+ # Disable delayed user input event handling
+ "dom.input_events.security.minNumTicks": 0,
+ # Disable delayed user input event handling
+ "dom.input_events.security.minTimeElapsedInMS": 0,
+ # Disable the ProcessHangMonitor
+ "dom.ipc.reportProcessHangs": False,
+ # No slow script dialogs
+ "dom.max_chrome_script_run_time": 0,
+ "dom.max_script_run_time": 0,
+ # Disable location change rate limitation
+ "dom.navigation.locationChangeRateLimit.count": 0,
+ # DOM Push
+ "dom.push.connection.enabled": False,
+ # Screen Orientation API
+ "dom.screenorientation.allow-lock": True,
+ # Disable dialog abuse if alerts are triggered too quickly
+ "dom.successive_dialog_time_limit": 0,
+ # Only load extensions from the application and user profile
+ # AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
+ "extensions.autoDisableScopes": 0,
+ "extensions.enabledScopes": 5,
+ # Disable metadata caching for installed add-ons by default
+ "extensions.getAddons.cache.enabled": False,
+ # Disable intalling any distribution add-ons
+ "extensions.installDistroAddons": False,
+ # Turn off extension updates so they don't bother tests
+ "extensions.update.enabled": False,
+ "extensions.update.notifyUser": False,
+ # Redirect various extension update URLs
+ "extensions.blocklist.detailsURL": (
+ "http://%(server)s/extensions-dummy/blocklistDetailsURL"
+ ),
+ "extensions.blocklist.itemURL": "http://%(server)s/extensions-dummy/blocklistItemURL",
+ "extensions.hotfix.url": "http://%(server)s/extensions-dummy/hotfixURL",
+ "extensions.systemAddon.update.url": "http://%(server)s/dummy-system-addons.xml",
+ "extensions.update.background.url": (
+ "http://%(server)s/extensions-dummy/updateBackgroundURL"
+ ),
+ "extensions.update.url": "http://%(server)s/extensions-dummy/updateURL",
+ # Make sure opening about:addons won"t hit the network
+ "extensions.getAddons.discovery.api_url": "data:, ",
+ "extensions.getAddons.get.url": "http://%(server)s/extensions-dummy/repositoryGetURL",
+ "extensions.getAddons.search.browseURL": (
+ "http://%(server)s/extensions-dummy/repositoryBrowseURL"
+ ),
+ # Allow the application to have focus even it runs in the background
+ "focusmanager.testmode": True,
+ # Disable useragent updates
+ "general.useragent.updates.enabled": False,
+ # Disable geolocation ping (#2)
+ "geo.provider.network.url": "",
+ # Always use network provider for geolocation tests
+ # so we bypass the OSX dialog raised by the corelocation provider
+ "geo.provider.testing": True,
+ # Do not scan Wifi
+ "geo.wifi.scan": False,
+ # Ensure webrender is on, no need for environment variables
+ "gfx.webrender.all": True,
+ # Disable idle-daily notifications to avoid expensive operations
+ # that may cause unexpected test timeouts.
+ "idle.lastDailyNotification": -1,
+ # Disable Firefox accounts ping
+ "identity.fxaccounts.auth.uri": "https://{server}/dummy/fxa",
+ # Disable download and usage of OpenH264, and Widevine plugins
+ "media.gmp-manager.updateEnabled": False,
+ # Disable the GFX sanity window
+ "media.sanity-test.disabled": True,
+ "media.volume_scale": "0.01",
+ # Disable connectivity service pings
+ "network.connectivity-service.enabled": False,
+ # Do not prompt for temporary redirects
+ "network.http.prompt-temp-redirect": False,
+ # Do not automatically switch between offline and online
+ "network.manage-offline-status": False,
+ # Make sure SNTP requests don't hit the network
+ "network.sntp.pools": "%(server)s",
+ # Privacy and Tracking Protection
+ "privacy.trackingprotection.enabled": False,
+ # Disable recommended automation prefs in CI
+ "remote.prefs.recommended": False,
+ # Don't do network connections for mitm priming
+ "security.certerrors.mitm.priming.enabled": False,
+ # Tests don't wait for the notification button security delay
+ "security.notification_enable_delay": 0,
+ # Do not download intermediate certificates
+ "security.remote_settings.intermediates.enabled": False,
+ # Ensure blocklist updates don't hit the network
+ "services.settings.server": "data:,#remote-settings-dummy/v1",
+ # Disable password capture, so that tests that include forms aren"t
+ # influenced by the presence of the persistent doorhanger notification
+ "signon.rememberSignons": False,
+ # Prevent starting into safe mode after application crashes
+ "toolkit.startup.max_resumed_crashes": -1,
+ # Disable most telemetry pings
+ "toolkit.telemetry.server": "https://%(server)s/telemetry-dummy/",
+ # Disable window occlusion on Windows, see Bug 1802473.
+ "widget.windows.window_occlusion_tracking.enabled": False,
+ }
+
+ def __init__(
+ self,
+ host=None,
+ port=None,
+ bin=None,
+ profile=None,
+ addons=None,
+ app_args=None,
+ symbols_path=None,
+ gecko_log=None,
+ prefs=None,
+ workspace=None,
+ verbose=0,
+ headless=False,
+ ):
+ self.runner_class = Runner
+ self.app_args = app_args or []
+ self.runner = None
+ self.symbols_path = symbols_path
+ self.binary = bin
+
+ self.marionette_host = host
+ self.marionette_port = port
+ self.addons = addons
+ self.prefs = prefs
+ self.required_prefs = deepcopy(self.required_prefs)
+ if prefs:
+ self.required_prefs.update(prefs)
+
+ self._gecko_log_option = gecko_log
+ self._gecko_log = None
+ self.verbose = verbose
+ self.headless = headless
+
+ # keep track of errors to decide whether instance is unresponsive
+ self.unresponsive_count = 0
+
+ # Alternative to default temporary directory
+ self.workspace = workspace
+
+ # Don't use the 'profile' property here, because sub-classes could add
+ # further preferences and data, which would not be included in the new
+ # profile
+ self._profile = profile
+
+ @property
+ def gecko_log(self):
+ if self._gecko_log:
+ return self._gecko_log
+
+ path = self._gecko_log_option
+ if path != "-":
+ if path is None:
+ path = "gecko.log"
+ elif os.path.isdir(path):
+ fname = "gecko-{}.log".format(time.time())
+ path = os.path.join(path, fname)
+
+ path = os.path.realpath(path)
+ if os.access(path, os.F_OK):
+ os.remove(path)
+
+ self._gecko_log = path
+ return self._gecko_log
+
+ @property
+ def profile(self):
+ return self._profile
+
+ @profile.setter
+ def profile(self, value):
+ self._update_profile(value)
+
+ def _update_profile(self, profile=None, profile_name=None):
+ """Check if the profile has to be created, or replaced.
+
+ :param profile: A Profile instance to be used.
+ :param name: Profile name to be used in the path.
+ """
+ if self.runner and self.runner.is_running():
+ raise errors.MarionetteException(
+ "The current profile can only be updated "
+ "when the instance is not running"
+ )
+
+ if isinstance(profile, Profile):
+ # Only replace the profile if it is not the current one
+ if hasattr(self, "_profile") and profile is self._profile:
+ return
+
+ else:
+ profile_args = self.profile_args
+ profile_path = profile
+
+ # If a path to a profile is given then clone it
+ if isinstance(profile_path, six.string_types):
+ profile_args["path_from"] = profile_path
+ profile_args["path_to"] = tempfile.mkdtemp(
+ suffix=".{}".format(profile_name or os.path.basename(profile_path)),
+ dir=self.workspace,
+ )
+ # The target must not exist yet
+ os.rmdir(profile_args["path_to"])
+
+ profile = Profile.clone(**profile_args)
+
+ # Otherwise create a new profile
+ else:
+ profile_args["profile"] = tempfile.mkdtemp(
+ suffix=".{}".format(profile_name or "mozrunner"),
+ dir=self.workspace,
+ )
+ profile = Profile(**profile_args)
+ profile.create_new = True
+
+ if isinstance(self.profile, Profile):
+ self.profile.cleanup()
+
+ self._profile = profile
+
+ def switch_profile(self, profile_name=None, clone_from=None):
+ """Switch the profile by using the given name, and optionally clone it.
+
+ Compared to :attr:`profile` this method allows to switch the profile
+ by giving control over the profile name as used for the new profile. It
+ also always creates a new blank profile, or as clone of an existent one.
+
+ :param profile_name: Optional, name of the profile, which will be used
+ as part of the profile path (folder name containing the profile).
+ :clone_from: Optional, if specified the new profile will be cloned
+ based on the given profile. This argument can be an instance of
+ ``mozprofile.Profile``, or the path of the profile.
+ """
+ if isinstance(clone_from, Profile):
+ clone_from = clone_from.profile
+
+ self._update_profile(clone_from, profile_name=profile_name)
+
+ @property
+ def profile_args(self):
+ args = {"preferences": deepcopy(self.required_prefs)}
+ args["preferences"]["marionette.port"] = self.marionette_port
+ args["preferences"]["marionette.defaultPrefs.port"] = self.marionette_port
+
+ if self.prefs:
+ args["preferences"].update(self.prefs)
+
+ if self.verbose:
+ level = "Trace" if self.verbose >= 2 else "Debug"
+ args["preferences"]["remote.log.level"] = level
+
+ if "-jsdebugger" in self.app_args:
+ args["preferences"].update(
+ {
+ "devtools.browsertoolbox.panel": "jsdebugger",
+ "devtools.chrome.enabled": True,
+ "devtools.debugger.prompt-connection": False,
+ "devtools.debugger.remote-enabled": True,
+ "devtools.testing": True,
+ }
+ )
+
+ if self.addons:
+ args["addons"] = self.addons
+
+ return args
+
+ @classmethod
+ def create(cls, app=None, *args, **kwargs):
+ try:
+ if not app and kwargs["bin"] is not None:
+ app_id = mozversion.get_version(binary=kwargs["bin"])["application_id"]
+ app = app_ids[app_id]
+
+ instance_class = apps[app]
+ except (IOError, KeyError):
+ exc, val, tb = sys.exc_info()
+ msg = 'Application "{0}" unknown (should be one of {1})'.format(
+ app, list(apps.keys())
+ )
+ reraise(NotImplementedError, NotImplementedError(msg), tb)
+
+ return instance_class(*args, **kwargs)
+
+ def start(self):
+ self._update_profile(self.profile)
+ self.runner = self.runner_class(**self._get_runner_args())
+ self.runner.start()
+
+ def _get_runner_args(self):
+ process_args = {
+ "processOutputLine": [NullOutput()],
+ "universal_newlines": True,
+ }
+
+ if self.gecko_log == "-":
+ if hasattr(sys.stdout, "buffer"):
+ process_args["stream"] = codecs.getwriter("utf-8")(sys.stdout.buffer)
+ else:
+ process_args["stream"] = codecs.getwriter("utf-8")(sys.stdout)
+ else:
+ process_args["logfile"] = self.gecko_log
+
+ env = os.environ.copy()
+
+ # Store all required preferences for tests which need to create clean profiles.
+ required_prefs_keys = list(self.required_prefs.keys())
+ env["MOZ_MARIONETTE_REQUIRED_PREFS"] = json.dumps(required_prefs_keys)
+
+ if self.headless:
+ env["MOZ_HEADLESS"] = "1"
+ env["DISPLAY"] = "77" # Set a fake display.
+
+ # environment variables needed for crashreporting
+ # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
+ env.update(
+ {
+ "MOZ_CRASHREPORTER": "1",
+ "MOZ_CRASHREPORTER_NO_REPORT": "1",
+ "MOZ_CRASHREPORTER_SHUTDOWN": "1",
+ }
+ )
+
+ return {
+ "binary": self.binary,
+ "profile": self.profile,
+ "cmdargs": ["-no-remote", "-marionette"] + self.app_args,
+ "env": env,
+ "symbols_path": self.symbols_path,
+ "process_args": process_args,
+ }
+
+ def close(self, clean=False):
+ """
+ Close the managed Gecko process.
+
+ Depending on self.runner_class, setting `clean` to True may also kill
+ the emulator process in which this instance is running.
+
+ :param clean: If True, also perform runner cleanup.
+ """
+ if self.runner:
+ self.runner.stop()
+ if clean:
+ self.runner.cleanup()
+
+ if clean:
+ if isinstance(self.profile, Profile):
+ self.profile.cleanup()
+ self.profile = None
+
+ def restart(self, prefs=None, clean=True):
+ """
+ Close then start the managed Gecko process.
+
+ :param prefs: Dictionary of preference names and values.
+ :param clean: If True, reset the profile before starting.
+ """
+ if prefs:
+ self.prefs = prefs
+ else:
+ self.prefs = None
+
+ self.close(clean=clean)
+ self.start()
+
+
+class FennecInstance(GeckoInstance):
+ fennec_prefs = {
+ # Enable output for dump() and chrome console API
+ "browser.dom.window.dump.enabled": True,
+ "devtools.console.stdout.chrome": True,
+ # Disable safe browsing / tracking protection updates
+ "browser.safebrowsing.update.enabled": False,
+ # Do not restore the last open set of tabs if the browser has crashed
+ "browser.sessionstore.resume_from_crash": False,
+ }
+
+ def __init__(
+ self,
+ emulator_binary=None,
+ avd_home=None,
+ avd=None,
+ adb_path=None,
+ serial=None,
+ connect_to_running_emulator=False,
+ package_name=None,
+ env=None,
+ *args,
+ **kwargs
+ ):
+ required_prefs = deepcopy(FennecInstance.fennec_prefs)
+ required_prefs.update(kwargs.get("prefs", {}))
+
+ super(FennecInstance, self).__init__(*args, **kwargs)
+ self.required_prefs.update(required_prefs)
+
+ self.runner_class = FennecEmulatorRunner
+ # runner args
+ self._package_name = package_name
+ self.emulator_binary = emulator_binary
+ self.avd_home = avd_home
+ self.adb_path = adb_path
+ self.avd = avd
+ self.env = env
+ self.serial = serial
+ self.connect_to_running_emulator = connect_to_running_emulator
+
+ @property
+ def package_name(self):
+ """
+ Name of app to run on emulator.
+
+ Note that FennecInstance does not use self.binary
+ """
+ if self._package_name is None:
+ self._package_name = "org.mozilla.fennec"
+ user = os.getenv("USER")
+ if user:
+ self._package_name += "_" + user
+ return self._package_name
+
+ def start(self):
+ self._update_profile(self.profile)
+ self.runner = self.runner_class(**self._get_runner_args())
+ try:
+ if self.connect_to_running_emulator:
+ self.runner.device.connect()
+ self.runner.start()
+ except Exception:
+ exc_cls, exc, tb = sys.exc_info()
+ reraise(
+ exc_cls,
+ exc_cls("Error possibly due to runner or device args: {}".format(exc)),
+ tb,
+ )
+
+ # forward marionette port
+ self.runner.device.device.forward(
+ local="tcp:{}".format(self.marionette_port),
+ remote="tcp:{}".format(self.marionette_port),
+ )
+
+ def _get_runner_args(self):
+ process_args = {
+ "processOutputLine": [NullOutput()],
+ "universal_newlines": True,
+ }
+
+ env = {} if self.env is None else self.env.copy()
+
+ runner_args = {
+ "app": self.package_name,
+ "avd_home": self.avd_home,
+ "adb_path": self.adb_path,
+ "binary": self.emulator_binary,
+ "env": env,
+ "profile": self.profile,
+ "cmdargs": ["-marionette"] + self.app_args,
+ "symbols_path": self.symbols_path,
+ "process_args": process_args,
+ "logdir": self.workspace or os.getcwd(),
+ "serial": self.serial,
+ }
+ if self.avd:
+ runner_args["avd"] = self.avd
+
+ return runner_args
+
+ def close(self, clean=False):
+ """
+ Close the managed Gecko process.
+
+ If `clean` is True and the Fennec instance is running in an
+ emulator managed by mozrunner, this will stop the emulator.
+
+ :param clean: If True, also perform runner cleanup.
+ """
+ super(FennecInstance, self).close(clean)
+ if clean and self.runner and self.runner.device.connected:
+ try:
+ self.runner.device.device.remove_forwards(
+ "tcp:{}".format(self.marionette_port)
+ )
+ self.unresponsive_count = 0
+ except Exception:
+ self.unresponsive_count += 1
+ traceback.print_exception(*sys.exc_info())
+
+
+class DesktopInstance(GeckoInstance):
+ desktop_prefs = {
+ # Disable Firefox old build background check
+ "app.update.checkInstallTime": False,
+ # Disable automatically upgrading Firefox
+ #
+ # Note: Possible update tests could reset or flip the value to allow
+ # updates to be downloaded and applied.
+ "app.update.disabledForTesting": True,
+ # !!! For backward compatibility up to Firefox 64. Only remove
+ # when this Firefox version is no longer supported by the client !!!
+ "app.update.auto": False,
+ # Don't show the content blocking introduction panel
+ # We use a larger number than the default 22 to have some buffer
+ # This can be removed once Firefox 69 and 68 ESR and are no longer supported.
+ "browser.contentblocking.introCount": 99,
+ # Enable output for dump() and chrome console API
+ "browser.dom.window.dump.enabled": True,
+ "devtools.console.stdout.chrome": True,
+ # Indicate that the download panel has been shown once so that whichever
+ # download test runs first doesn"t show the popup inconsistently
+ "browser.download.panel.shown": True,
+ # Do not show the EULA notification which can interfer with tests
+ "browser.EULA.override": True,
+ # Disable Activity Stream telemetry pings
+ "browser.newtabpage.activity-stream.telemetry": False,
+ # Always display a blank page
+ "browser.newtabpage.enabled": False,
+ # Background thumbnails in particular cause grief, and disabling thumbnails
+ # in general can"t hurt - we re-enable them when tests need them
+ "browser.pagethumbnails.capturing_disabled": True,
+ # Disable safe browsing / tracking protection updates
+ "browser.safebrowsing.update.enabled": False,
+ # Disable updates to search engines
+ "browser.search.update": False,
+ # Do not restore the last open set of tabs if the browser has crashed
+ "browser.sessionstore.resume_from_crash": False,
+ # Don't check for the default web browser during startup
+ "browser.shell.checkDefaultBrowser": False,
+ # Disable session restore infobar
+ "browser.startup.couldRestoreSession.count": -1,
+ # Needed for branded builds to prevent opening a second tab on startup
+ "browser.startup.homepage_override.mstone": "ignore",
+ # Start with a blank page by default
+ "browser.startup.page": 0,
+ # Don't unload tabs when available memory is running low
+ "browser.tabs.unloadOnLowMemory": False,
+ # Do not warn when closing all open tabs
+ "browser.tabs.warnOnClose": False,
+ # Do not warn when closing all other open tabs
+ "browser.tabs.warnOnCloseOtherTabs": False,
+ # Do not warn when multiple tabs will be opened
+ "browser.tabs.warnOnOpen": False,
+ # Don't show the Bookmarks Toolbar on any tab (the above pref that
+ # disables the New Tab Page ends up showing the toolbar on about:blank).
+ "browser.toolbars.bookmarks.visibility": "never",
+ # Disable the UI tour
+ "browser.uitour.enabled": False,
+ # Turn off Merino suggestions in the location bar so as not to trigger network
+ # connections.
+ "browser.urlbar.merino.endpointURL": "",
+ # Turn off search suggestions in the location bar so as not to trigger network
+ # connections.
+ "browser.urlbar.suggest.searches": False,
+ # Don't warn when exiting the browser
+ "browser.warnOnQuit": False,
+ # Disable first-run welcome page
+ "startup.homepage_welcome_url": "about:blank",
+ "startup.homepage_welcome_url.additional": "",
+ }
+
+ def __init__(self, *args, **kwargs):
+ required_prefs = deepcopy(DesktopInstance.desktop_prefs)
+ required_prefs.update(kwargs.get("prefs", {}))
+
+ super(DesktopInstance, self).__init__(*args, **kwargs)
+ self.required_prefs.update(required_prefs)
+
+
+class ThunderbirdInstance(GeckoInstance):
+ def __init__(self, *args, **kwargs):
+ super(ThunderbirdInstance, self).__init__(*args, **kwargs)
+ try:
+ # Copied alongside in the test archive
+ from .thunderbirdinstance import thunderbird_prefs
+ except ImportError:
+ try:
+ # Coming from source tree through virtualenv
+ from thunderbirdinstance import thunderbird_prefs
+ except ImportError:
+ thunderbird_prefs = {}
+ self.required_prefs.update(thunderbird_prefs)
+
+
+class NullOutput(object):
+ def __call__(self, line):
+ pass
+
+
+apps = {
+ "fennec": FennecInstance,
+ "fxdesktop": DesktopInstance,
+ "thunderbird": ThunderbirdInstance,
+}
+
+app_ids = {
+ "{aa3c5121-dab2-40e2-81ca-7ea25febc110}": "fennec",
+ "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "fxdesktop",
+ "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "thunderbird",
+}
diff --git a/testing/marionette/client/marionette_driver/keys.py b/testing/marionette/client/marionette_driver/keys.py
new file mode 100644
index 0000000000..18b547caa7
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/keys.py
@@ -0,0 +1,87 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# copyright 2008-2009 WebDriver committers
+# Copyright 2008-2009 Google Inc.
+#
+# Licensed under the Apache License Version 2.0 = uthe "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.
+
+
+class Keys(object):
+ NULL = "\ue000"
+ CANCEL = "\ue001" # ^break
+ HELP = "\ue002"
+ BACK_SPACE = "\ue003"
+ TAB = "\ue004"
+ CLEAR = "\ue005"
+ RETURN = "\ue006"
+ ENTER = "\ue007"
+ SHIFT = "\ue008"
+ LEFT_SHIFT = "\ue008" # alias
+ CONTROL = "\ue009"
+ LEFT_CONTROL = "\ue009" # alias
+ ALT = "\ue00a"
+ LEFT_ALT = "\ue00a" # alias
+ PAUSE = "\ue00b"
+ ESCAPE = "\ue00c"
+ SPACE = "\ue00d"
+ PAGE_UP = "\ue00e"
+ PAGE_DOWN = "\ue00f"
+ END = "\ue010"
+ HOME = "\ue011"
+ LEFT = "\ue012"
+ ARROW_LEFT = "\ue012" # alias
+ UP = "\ue013"
+ ARROW_UP = "\ue013" # alias
+ RIGHT = "\ue014"
+ ARROW_RIGHT = "\ue014" # alias
+ DOWN = "\ue015"
+ ARROW_DOWN = "\ue015" # alias
+ INSERT = "\ue016"
+ DELETE = "\ue017"
+ SEMICOLON = "\ue018"
+ EQUALS = "\ue019"
+
+ NUMPAD0 = "\ue01a" # numbe pad keys
+ NUMPAD1 = "\ue01b"
+ NUMPAD2 = "\ue01c"
+ NUMPAD3 = "\ue01d"
+ NUMPAD4 = "\ue01e"
+ NUMPAD5 = "\ue01f"
+ NUMPAD6 = "\ue020"
+ NUMPAD7 = "\ue021"
+ NUMPAD8 = "\ue022"
+ NUMPAD9 = "\ue023"
+ MULTIPLY = "\ue024"
+ ADD = "\ue025"
+ SEPARATOR = "\ue026"
+ SUBTRACT = "\ue027"
+ DECIMAL = "\ue028"
+ DIVIDE = "\ue029"
+
+ F1 = "\ue031" # function keys
+ F2 = "\ue032"
+ F3 = "\ue033"
+ F4 = "\ue034"
+ F5 = "\ue035"
+ F6 = "\ue036"
+ F7 = "\ue037"
+ F8 = "\ue038"
+ F9 = "\ue039"
+ F10 = "\ue03a"
+ F11 = "\ue03b"
+ F12 = "\ue03c"
+
+ META = "\ue03d"
+ COMMAND = "\ue03d"
diff --git a/testing/marionette/client/marionette_driver/localization.py b/testing/marionette/client/marionette_driver/localization.py
new file mode 100644
index 0000000000..fccb32f416
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/localization.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/.
+
+
+class L10n(object):
+ """An API which allows Marionette to handle localized content.
+
+ The `localization`_ of UI elements in Gecko based applications is done via
+ entities and properties. For static values entities are used, which are located
+ in .dtd files. Whereby for dynamically updated content the values come from
+ .property files. Both types of elements can be identifed via a unique id,
+ and the translated content retrieved.
+
+ For example::
+
+ from marionette_driver.localization import L10n
+ l10n = L10n(marionette)
+
+ l10n.localize_entity(["chrome://branding/locale/brand.dtd"], "brandShortName")
+ l10n.localize_property(["chrome://global/locale/findbar.properties"], "FastFind"))
+
+ .. _localization: https://mzl.la/2eUMjyF
+ """
+
+ def __init__(self, marionette):
+ self._marionette = marionette
+
+ def localize_entity(self, dtd_urls, entity_id):
+ """Retrieve the localized string for the specified entity id.
+
+ :param dtd_urls: List of .dtd URLs which will be used to search for the entity.
+ :param entity_id: ID of the entity to retrieve the localized string for.
+
+ :returns: The localized string for the requested entity.
+ :raises: :exc:`NoSuchElementException`
+ """
+ body = {"urls": dtd_urls, "id": entity_id}
+ return self._marionette._send_message("L10n:LocalizeEntity", body, key="value")
+
+ def localize_property(self, properties_urls, property_id):
+ """Retrieve the localized string for the specified property id.
+
+ :param properties_urls: List of .properties URLs which will be used to
+ search for the property.
+ :param property_id: ID of the property to retrieve the localized string for.
+
+ :returns: The localized string for the requested property.
+ :raises: :exc:`NoSuchElementException`
+ """
+ body = {"urls": properties_urls, "id": property_id}
+ return self._marionette._send_message(
+ "L10n:LocalizeProperty", body, key="value"
+ )
diff --git a/testing/marionette/client/marionette_driver/marionette.py b/testing/marionette/client/marionette_driver/marionette.py
new file mode 100644
index 0000000000..3fbc1b63d7
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -0,0 +1,2183 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 datetime
+import json
+import os
+import socket
+import sys
+import time
+import traceback
+from contextlib import contextmanager
+
+import six
+from six import reraise
+
+from . import errors, transport
+from .decorators import do_process_check
+from .geckoinstance import GeckoInstance
+from .keys import Keys
+from .timeout import Timeouts
+
+WEB_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
+WEB_FRAME_KEY = "frame-075b-4da1-b6ba-e579c2d3230a"
+WEB_SHADOW_ROOT_KEY = "shadow-6066-11e4-a52e-4f735466cecf"
+WEB_WINDOW_KEY = "window-fcc6-11e5-b4f8-330a88ab9d7f"
+
+
+class MouseButton(object):
+ """Enum-like class for mouse button constants."""
+
+ LEFT = 0
+ MIDDLE = 1
+ RIGHT = 2
+
+
+class ActionSequence(object):
+ r"""API for creating and performing action sequences.
+
+ Each action method adds one or more actions to a queue. When perform()
+ is called, the queued actions fire in order.
+
+ May be chained together as in::
+
+ ActionSequence(self.marionette, "key", id) \
+ .key_down("a") \
+ .key_up("a") \
+ .perform()
+ """
+
+ def __init__(self, marionette, action_type, input_id, pointer_params=None):
+ self.marionette = marionette
+ self._actions = []
+ self._id = input_id
+ self._pointer_params = pointer_params
+ self._type = action_type
+
+ @property
+ def dict(self):
+ d = {
+ "type": self._type,
+ "id": self._id,
+ "actions": self._actions,
+ }
+ if self._pointer_params is not None:
+ d["parameters"] = self._pointer_params
+ return d
+
+ def perform(self):
+ """Perform all queued actions."""
+ self.marionette.actions.perform([self.dict])
+
+ def _key_action(self, subtype, value):
+ self._actions.append({"type": subtype, "value": value})
+
+ def _pointer_action(self, subtype, button):
+ self._actions.append({"type": subtype, "button": button})
+
+ def pause(self, duration):
+ self._actions.append({"type": "pause", "duration": duration})
+ return self
+
+ def pointer_move(self, x, y, duration=None, origin=None):
+ """Queue a pointerMove action.
+
+ :param x: Destination x-axis coordinate of pointer in CSS pixels.
+ :param y: Destination y-axis coordinate of pointer in CSS pixels.
+ :param duration: Number of milliseconds over which to distribute the
+ move. If None, remote end defaults to 0.
+ :param origin: Origin of coordinates, either "viewport", "pointer" or
+ an Element. If None, remote end defaults to "viewport".
+ """
+ action = {"type": "pointerMove", "x": x, "y": y}
+ if duration is not None:
+ action["duration"] = duration
+ if origin is not None:
+ if isinstance(origin, WebElement):
+ action["origin"] = {origin.kind: origin.id}
+ else:
+ action["origin"] = origin
+ self._actions.append(action)
+ return self
+
+ def pointer_up(self, button=MouseButton.LEFT):
+ """Queue a pointerUp action for `button`.
+
+ :param button: Pointer button to perform action with.
+ Default: 0, which represents main device button.
+ """
+ self._pointer_action("pointerUp", button)
+ return self
+
+ def pointer_down(self, button=MouseButton.LEFT):
+ """Queue a pointerDown action for `button`.
+
+ :param button: Pointer button to perform action with.
+ Default: 0, which represents main device button.
+ """
+ self._pointer_action("pointerDown", button)
+ return self
+
+ def click(self, element=None, button=MouseButton.LEFT):
+ """Queue a click with the specified button.
+
+ If an element is given, move the pointer to that element first,
+ otherwise click current pointer coordinates.
+
+ :param element: Optional element to click.
+ :param button: Integer representing pointer button to perform action
+ with. Default: 0, which represents main device button.
+ """
+ if element:
+ self.pointer_move(0, 0, origin=element)
+ return self.pointer_down(button).pointer_up(button)
+
+ def key_down(self, value):
+ """Queue a keyDown action for `value`.
+
+ :param value: Single character to perform key action with.
+ """
+ self._key_action("keyDown", value)
+ return self
+
+ def key_up(self, value):
+ """Queue a keyUp action for `value`.
+
+ :param value: Single character to perform key action with.
+ """
+ self._key_action("keyUp", value)
+ return self
+
+ def scroll(self, x, y, delta_x, delta_y, duration=None, origin=None):
+ """Queue a scroll action.
+
+ :param x: Destination x-axis coordinate of pointer in CSS pixels.
+ :param y: Destination y-axis coordinate of pointer in CSS pixels.
+ :param delta_x: Scroll delta for x-axis in CSS pixels.
+ :param delta_y: Scroll delta for y-axis in CSS pixels.
+ :param duration: Number of milliseconds over which to distribute the
+ scroll. If None, remote end defaults to 0.
+ :param origin: Origin of coordinates, either "viewport", "pointer" or
+ an Element. If None, remote end defaults to "viewport".
+ """
+ action = {
+ "type": "scroll",
+ "x": x,
+ "y": y,
+ "deltaX": delta_x,
+ "deltaY": delta_y,
+ }
+
+ if duration is not None:
+ action["duration"] = duration
+ if origin is not None:
+ if isinstance(origin, WebElement):
+ action["origin"] = {origin.kind: origin.id}
+ else:
+ action["origin"] = origin
+ self._actions.append(action)
+ return self
+
+ def send_keys(self, keys):
+ """Queue a keyDown and keyUp action for each character in `keys`.
+
+ :param keys: String of keys to perform key actions with.
+ """
+ for c in keys:
+ self.key_down(c)
+ self.key_up(c)
+ return self
+
+
+class Actions(object):
+ def __init__(self, marionette):
+ self.marionette = marionette
+
+ def perform(self, actions=None):
+ """Perform actions by tick from each action sequence in `actions`.
+
+ :param actions: List of input source action sequences. A single action
+ sequence may be created with the help of
+ ``ActionSequence.dict``.
+ """
+ body = {"actions": [] if actions is None else actions}
+ return self.marionette._send_message("WebDriver:PerformActions", body)
+
+ def release(self):
+ return self.marionette._send_message("WebDriver:ReleaseActions")
+
+ def sequence(self, *args, **kwargs):
+ """Return an empty ActionSequence of the designated type.
+
+ See ActionSequence for parameter list.
+ """
+ return ActionSequence(self.marionette, *args, **kwargs)
+
+
+class WebElement(object):
+ """Represents a DOM Element."""
+
+ identifiers = (WEB_ELEMENT_KEY,)
+
+ def __init__(self, marionette, id, kind=WEB_ELEMENT_KEY):
+ self.marionette = marionette
+ assert id is not None
+ self.id = id
+ self.kind = kind
+
+ def __str__(self):
+ return self.id
+
+ def __eq__(self, other_element):
+ return self.id == other_element.id
+
+ def __hash__(self):
+ # pylint --py3k: W1641
+ return hash(self.id)
+
+ def find_element(self, method, target):
+ """Returns an ``WebElement`` instance that matches the specified
+ method and target, relative to the current element.
+
+ For more details on this function, see the
+ :func:`~marionette_driver.marionette.Marionette.find_element` method
+ in the Marionette class.
+ """
+ return self.marionette.find_element(method, target, self.id)
+
+ def find_elements(self, method, target):
+ """Returns a list of all ``WebElement`` instances that match the
+ specified method and target in the current context.
+
+ For more details on this function, see the
+ :func:`~marionette_driver.marionette.Marionette.find_elements` method
+ in the Marionette class.
+ """
+ return self.marionette.find_elements(method, target, self.id)
+
+ def get_attribute(self, name):
+ """Returns the requested attribute, or None if no attribute
+ is set.
+ """
+ body = {"id": self.id, "name": name}
+ return self.marionette._send_message(
+ "WebDriver:GetElementAttribute", body, key="value"
+ )
+
+ def get_property(self, name):
+ """Returns the requested property, or None if the property is
+ not set.
+ """
+ try:
+ body = {"id": self.id, "name": name}
+ return self.marionette._send_message(
+ "WebDriver:GetElementProperty", body, key="value"
+ )
+ except errors.UnknownCommandException:
+ # Keep backward compatibility for code which uses get_attribute() to
+ # also retrieve element properties.
+ # Remove when Firefox 55 is stable.
+ return self.get_attribute(name)
+
+ def click(self):
+ """Simulates a click on the element."""
+ self.marionette._send_message("WebDriver:ElementClick", {"id": self.id})
+
+ @property
+ def text(self):
+ """Returns the visible text of the element, and its child elements."""
+ body = {"id": self.id}
+ return self.marionette._send_message(
+ "WebDriver:GetElementText", body, key="value"
+ )
+
+ def send_keys(self, *strings):
+ """Sends the string via synthesized keypresses to the element.
+ If an array is passed in like `marionette.send_keys(Keys.SHIFT, "a")` it
+ will be joined into a string.
+ If an integer is passed in like `marionette.send_keys(1234)` it will be
+ coerced into a string.
+ """
+ keys = Marionette.convert_keys(*strings)
+ self.marionette._send_message(
+ "WebDriver:ElementSendKeys", {"id": self.id, "text": keys}
+ )
+
+ def clear(self):
+ """Clears the input of the element."""
+ self.marionette._send_message("WebDriver:ElementClear", {"id": self.id})
+
+ def is_selected(self):
+ """Returns True if the element is selected."""
+ body = {"id": self.id}
+ return self.marionette._send_message(
+ "WebDriver:IsElementSelected", body, key="value"
+ )
+
+ def is_enabled(self):
+ """This command will return False if all the following criteria
+ are met otherwise return True:
+
+ * A form control is disabled.
+ * A ``WebElement`` has a disabled boolean attribute.
+ """
+ body = {"id": self.id}
+ return self.marionette._send_message(
+ "WebDriver:IsElementEnabled", body, key="value"
+ )
+
+ def is_displayed(self):
+ """Returns True if the element is displayed, False otherwise."""
+ body = {"id": self.id}
+ return self.marionette._send_message(
+ "WebDriver:IsElementDisplayed", body, key="value"
+ )
+
+ @property
+ def tag_name(self):
+ """The tag name of the element."""
+ body = {"id": self.id}
+ return self.marionette._send_message(
+ "WebDriver:GetElementTagName", body, key="value"
+ )
+
+ @property
+ def rect(self):
+ """Gets the element's bounding rectangle.
+
+ This will return a dictionary with the following:
+
+ * x and y represent the top left coordinates of the ``WebElement``
+ relative to top left corner of the document.
+ * height and the width will contain the height and the width
+ of the DOMRect of the ``WebElement``.
+ """
+ return self.marionette._send_message(
+ "WebDriver:GetElementRect", {"id": self.id}
+ )
+
+ def value_of_css_property(self, property_name):
+ """Gets the value of the specified CSS property name.
+
+ :param property_name: Property name to get the value of.
+ """
+ body = {"id": self.id, "propertyName": property_name}
+ return self.marionette._send_message(
+ "WebDriver:GetElementCSSValue", body, key="value"
+ )
+
+ @property
+ def shadow_root(self):
+ """Gets the shadow root of the current element"""
+ return self.marionette._send_message(
+ "WebDriver:GetShadowRoot", {"id": self.id}, key="value"
+ )
+
+ @property
+ def computed_label(self):
+ """Gets the computed accessibility label of the current element"""
+ return self.marionette._send_message(
+ "WebDriver:GetComputedLabel", {"id": self.id}, key="value"
+ )
+
+ @property
+ def computed_role(self):
+ """Gets the computed accessibility role of the current element"""
+ return self.marionette._send_message(
+ "WebDriver:GetComputedRole", {"id": self.id}, key="value"
+ )
+
+ @classmethod
+ def _from_json(cls, json, marionette):
+ if isinstance(json, dict):
+ if WEB_ELEMENT_KEY in json:
+ return cls(marionette, json[WEB_ELEMENT_KEY], WEB_ELEMENT_KEY)
+ raise ValueError("Unrecognised web element")
+
+
+class ShadowRoot(object):
+ """A Class to handling Shadow Roots"""
+
+ identifiers = (WEB_SHADOW_ROOT_KEY,)
+
+ def __init__(self, marionette, id, kind=WEB_SHADOW_ROOT_KEY):
+ self.marionette = marionette
+ assert id is not None
+ self.id = id
+ self.kind = kind
+
+ def __str__(self):
+ return self.id
+
+ def __eq__(self, other_element):
+ return self.id == other_element.id
+
+ def __hash__(self):
+ # pylint --py3k: W1641
+ return hash(self.id)
+
+ def find_element(self, method, target):
+ """Returns a ``WebElement`` instance that matches the specified
+ method and target, relative to the current shadow root.
+
+ For more details on this function, see the
+ :func:`~marionette_driver.marionette.Marionette.find_element` method
+ in the Marionette class.
+ """
+ body = {"shadowRoot": self.id, "value": target, "using": method}
+ return self.marionette._send_message(
+ "WebDriver:FindElementFromShadowRoot", body, key="value"
+ )
+
+ def find_elements(self, method, target):
+ """Returns a list of all ``WebElement`` instances that match the
+ specified method and target in the current shadow root.
+
+ For more details on this function, see the
+ :func:`~marionette_driver.marionette.Marionette.find_elements` method
+ in the Marionette class.
+ """
+ body = {"shadowRoot": self.id, "value": target, "using": method}
+ return self.marionette._send_message(
+ "WebDriver:FindElementsFromShadowRoot", body
+ )
+
+ @classmethod
+ def _from_json(cls, json, marionette):
+ if isinstance(json, dict):
+ if WEB_SHADOW_ROOT_KEY in json:
+ return cls(marionette, json[WEB_SHADOW_ROOT_KEY])
+ raise ValueError("Unrecognised shadow root")
+
+
+class WebFrame(object):
+ """A Class to handle frame windows"""
+
+ identifiers = (WEB_FRAME_KEY,)
+
+ def __init__(self, marionette, id, kind=WEB_FRAME_KEY):
+ self.marionette = marionette
+ assert id is not None
+ self.id = id
+ self.kind = kind
+
+ def __str__(self):
+ return self.id
+
+ def __eq__(self, other_element):
+ return self.id == other_element.id
+
+ def __hash__(self):
+ # pylint --py3k: W1641
+ return hash(self.id)
+
+ @classmethod
+ def _from_json(cls, json, marionette):
+ if isinstance(json, dict):
+ if WEB_FRAME_KEY in json:
+ return cls(marionette, json[WEB_FRAME_KEY])
+ raise ValueError("Unrecognised web frame")
+
+
+class WebWindow(object):
+ """A Class to handle top-level windows"""
+
+ identifiers = (WEB_WINDOW_KEY,)
+
+ def __init__(self, marionette, id, kind=WEB_WINDOW_KEY):
+ self.marionette = marionette
+ assert id is not None
+ self.id = id
+ self.kind = kind
+
+ def __str__(self):
+ return self.id
+
+ def __eq__(self, other_element):
+ return self.id == other_element.id
+
+ def __hash__(self):
+ # pylint --py3k: W1641
+ return hash(self.id)
+
+ @classmethod
+ def _from_json(cls, json, marionette):
+ if isinstance(json, dict):
+ if WEB_WINDOW_KEY in json:
+ return cls(marionette, json[WEB_WINDOW_KEY])
+ raise ValueError("Unrecognised web window")
+
+
+class Alert(object):
+ """A class for interacting with alerts.
+
+ ::
+
+ Alert(marionette).accept()
+ Alert(marionette).dismiss()
+ """
+
+ def __init__(self, marionette):
+ self.marionette = marionette
+
+ def accept(self):
+ """Accept a currently displayed modal dialog."""
+ self.marionette._send_message("WebDriver:AcceptAlert")
+
+ def dismiss(self):
+ """Dismiss a currently displayed modal dialog."""
+ self.marionette._send_message("WebDriver:DismissAlert")
+
+ @property
+ def text(self):
+ """Return the currently displayed text in a tab modal."""
+ return self.marionette._send_message("WebDriver:GetAlertText", key="value")
+
+ def send_keys(self, *string):
+ """Send keys to the currently displayed text input area in an open
+ tab modal dialog."""
+ self.marionette._send_message(
+ "WebDriver:SendAlertText", {"text": Marionette.convert_keys(*string)}
+ )
+
+
+class Marionette(object):
+ """Represents a Marionette connection to a browser or device."""
+
+ CONTEXT_CHROME = "chrome" # non-browser content: windows, dialogs, etc.
+ CONTEXT_CONTENT = "content" # browser content: iframes, divs, etc.
+ DEFAULT_STARTUP_TIMEOUT = 120
+ DEFAULT_SHUTDOWN_TIMEOUT = (
+ 70 # By default Firefox will kill hanging threads after 60s
+ )
+
+ # Bug 1336953 - Until we can remove the socket timeout parameter it has to be
+ # set a default value which is larger than the longest timeout as defined by the
+ # WebDriver spec. In that case its 300s for page load. Also add another minute
+ # so that slow builds have enough time to send the timeout error to the client.
+ DEFAULT_SOCKET_TIMEOUT = 360
+
+ def __init__(
+ self,
+ host="127.0.0.1",
+ port=2828,
+ app=None,
+ bin=None,
+ baseurl=None,
+ socket_timeout=None,
+ startup_timeout=None,
+ **instance_args
+ ):
+ """Construct a holder for the Marionette connection.
+
+ Remember to call ``start_session`` in order to initiate the
+ connection and start a Marionette session.
+
+ :param host: Host where the Marionette server listens.
+ Defaults to 127.0.0.1.
+ :param port: Port where the Marionette server listens.
+ Defaults to port 2828.
+ :param baseurl: Where to look for files served from Marionette's
+ www directory.
+ :param socket_timeout: Timeout for Marionette socket operations.
+ :param startup_timeout: Seconds to wait for a connection with
+ binary.
+ :param bin: Path to browser binary. If any truthy value is given
+ this will attempt to start a Gecko instance with the specified
+ `app`.
+ :param app: Type of ``instance_class`` to use for managing app
+ instance. See ``marionette_driver.geckoinstance``.
+ :param instance_args: Arguments to pass to ``instance_class``.
+ """
+ self.host = "127.0.0.1" # host
+ if int(port) == 0:
+ port = Marionette.check_port_available(port)
+ self.port = self.local_port = int(port)
+ self.bin = bin
+ self.client = None
+ self.instance = None
+ self.requested_capabilities = None
+ self.session = None
+ self.session_id = None
+ self.process_id = None
+ self.profile = None
+ self.window = None
+ self.chrome_window = None
+ self.baseurl = baseurl
+ self._test_name = None
+ self.crashed = 0
+ self.is_shutting_down = False
+ self.cleanup_ran = False
+
+ if socket_timeout is None:
+ self.socket_timeout = self.DEFAULT_SOCKET_TIMEOUT
+ else:
+ self.socket_timeout = float(socket_timeout)
+
+ if startup_timeout is None:
+ self.startup_timeout = self.DEFAULT_STARTUP_TIMEOUT
+ else:
+ self.startup_timeout = int(startup_timeout)
+
+ self.shutdown_timeout = self.DEFAULT_SHUTDOWN_TIMEOUT
+
+ if self.bin:
+ self.instance = GeckoInstance.create(
+ app, host=self.host, port=self.port, bin=self.bin, **instance_args
+ )
+ self.start_binary(self.startup_timeout)
+
+ self.actions = Actions(self)
+ self.timeout = Timeouts(self)
+
+ @property
+ def profile_path(self):
+ if self.instance and self.instance.profile:
+ return self.instance.profile.profile
+
+ def start_binary(self, timeout):
+ try:
+ self.check_port_available(self.port, host=self.host)
+ except socket.error:
+ _, value, tb = sys.exc_info()
+ msg = "Port {}:{} is unavailable ({})".format(self.host, self.port, value)
+ reraise(IOError, IOError(msg), tb)
+
+ try:
+ self.instance.start()
+ self.raise_for_port(timeout=timeout)
+ except socket.timeout:
+ # Something went wrong with starting up Marionette server. Given
+ # that the process will not quit itself, force a shutdown immediately.
+ self.cleanup()
+
+ msg = (
+ "Process killed after {}s because no connection to Marionette "
+ "server could be established. Check gecko.log for errors"
+ )
+ reraise(IOError, IOError(msg.format(timeout)), sys.exc_info()[2])
+
+ def cleanup(self):
+ if self.session is not None:
+ try:
+ self.delete_session()
+ except (errors.MarionetteException, IOError):
+ # These exceptions get thrown if the Marionette server
+ # hit an exception/died or the connection died. We can
+ # do no further server-side cleanup in this case.
+ pass
+
+ if self.instance:
+ # stop application and, if applicable, stop emulator
+ self.instance.close(clean=True)
+ if self.instance.unresponsive_count >= 3:
+ raise errors.UnresponsiveInstanceException(
+ "Application clean-up has failed >2 consecutive times."
+ )
+
+ self.cleanup_ran = True
+
+ def __del__(self):
+ if not self.cleanup_ran:
+ self.cleanup()
+
+ @staticmethod
+ def check_port_available(port, host=""):
+ """Check if "host:port" is available.
+
+ Raise socket.error if port is not available.
+ """
+ port = int(port)
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ s.bind((host, port))
+ port = s.getsockname()[1]
+ finally:
+ s.close()
+ return port
+
+ def raise_for_port(self, timeout=None, check_process_status=True):
+ """Raise socket.timeout if no connection can be established.
+
+ :param timeout: Optional timeout in seconds for the server to be ready.
+ :param check_process_status: Optional, if `True` the process will be
+ continuously checked if it has exited, and the connection
+ attempt will be aborted.
+ """
+ if timeout is None:
+ timeout = self.startup_timeout
+
+ runner = None
+ if self.instance is not None:
+ runner = self.instance.runner
+
+ poll_interval = 0.1
+ starttime = datetime.datetime.now()
+ timeout_time = starttime + datetime.timedelta(seconds=timeout)
+
+ client = transport.TcpTransport(self.host, self.port, 0.5)
+
+ connected = False
+ while datetime.datetime.now() < timeout_time:
+ # If the instance we want to connect to is not running return immediately
+ if check_process_status and runner is not None and not runner.is_running():
+ break
+
+ try:
+ client.connect()
+ return True
+ except socket.error:
+ pass
+ finally:
+ client.close()
+
+ time.sleep(poll_interval)
+
+ if not connected:
+ # There might have been a startup crash of the application
+ if runner is not None and self.check_for_crash() > 0:
+ raise IOError("Process crashed (Exit code: {})".format(runner.wait(0)))
+
+ raise socket.timeout(
+ "Timed out waiting for connection on {0}:{1}!".format(
+ self.host, self.port
+ )
+ )
+
+ @do_process_check
+ def _send_message(self, name, params=None, key=None):
+ """Send a blocking message to the server.
+
+ Marionette provides an asynchronous, non-blocking interface and
+ this attempts to paper over this by providing a synchronous API
+ to the user.
+
+ :param name: Requested command key.
+ :param params: Optional dictionary of key/value arguments.
+ :param key: Optional key to extract from response.
+
+ :returns: Full response from the server, or if `key` is given,
+ the value of said key in the response.
+ """
+ if not self.session_id and name != "WebDriver:NewSession":
+ raise errors.InvalidSessionIdException("Please start a session")
+
+ try:
+ msg = self.client.request(name, params)
+ except IOError:
+ self.delete_session(send_request=False)
+ raise
+
+ res, err = msg.result, msg.error
+ if err:
+ self._handle_error(err)
+
+ if key is not None:
+ return self._from_json(res.get(key))
+ else:
+ return self._from_json(res)
+
+ def _handle_error(self, obj):
+ error = obj["error"]
+ message = obj["message"]
+ stacktrace = obj["stacktrace"]
+
+ raise errors.lookup(error)(message, stacktrace=stacktrace)
+
+ def check_for_crash(self):
+ """Check if the process crashed.
+
+ :returns: True, if a crash happened since the method has been called the last time.
+ """
+ crash_count = 0
+
+ if self.instance:
+ name = self.test_name or "marionette.py"
+ crash_count = self.instance.runner.check_for_crashes(test_name=name)
+ self.crashed = self.crashed + crash_count
+
+ return crash_count > 0
+
+ def _handle_socket_failure(self):
+ """Handle socket failures for the currently connected application.
+
+ If the application crashed then clean-up internal states, or in case of a content
+ crash also kill the process. If there are other reasons for a socket failure,
+ wait for the process to shutdown itself, or force kill it.
+
+ Please note that the method expects an exception to be handled on the current stack
+ frame, and is only called via the `@do_process_check` decorator.
+
+ """
+ exc_cls, exc, tb = sys.exc_info()
+
+ # If the application hasn't been launched by Marionette no further action can be done.
+ # In such cases we simply re-throw the exception.
+ if not self.instance:
+ reraise(exc_cls, exc, tb)
+
+ else:
+ # Somehow the socket disconnected. Give the application some time to shutdown
+ # itself before killing the process.
+ returncode = self.instance.runner.wait(timeout=self.shutdown_timeout)
+
+ if returncode is None:
+ message = (
+ "Process killed because the connection to Marionette server is "
+ "lost. Check gecko.log for errors"
+ )
+ # This will force-close the application without sending any other message.
+ self.cleanup()
+ else:
+ # If Firefox quit itself check if there was a crash
+ crash_count = self.check_for_crash()
+
+ if crash_count > 0:
+ # SIGUSR1 indicates a forced shutdown due to a content process crash
+ if returncode == 245:
+ message = "Content process crashed"
+ else:
+ message = "Process crashed (Exit code: {returncode})"
+ else:
+ message = (
+ "Process has been unexpectedly closed (Exit code: {returncode})"
+ )
+
+ self.delete_session(send_request=False)
+
+ message += " (Reason: {reason})"
+
+ reraise(
+ IOError, IOError(message.format(returncode=returncode, reason=exc)), tb
+ )
+
+ @staticmethod
+ def convert_keys(*string):
+ typing = []
+ for val in string:
+ if isinstance(val, Keys):
+ typing.append(val)
+ elif isinstance(val, int):
+ val = str(val)
+ for i in range(len(val)):
+ typing.append(val[i])
+ else:
+ for i in range(len(val)):
+ typing.append(val[i])
+ return "".join(typing)
+
+ def clear_pref(self, pref):
+ """Clear the user-defined value from the specified preference.
+
+ :param pref: Name of the preference.
+ """
+ with self.using_context(self.CONTEXT_CHROME):
+ self.execute_script(
+ """
+ const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+ );
+ Preferences.reset(arguments[0]);
+ """,
+ script_args=(pref,),
+ )
+
+ def get_pref(self, pref, default_branch=False, value_type="unspecified"):
+ """Get the value of the specified preference.
+
+ :param pref: Name of the preference.
+ :param default_branch: Optional, if `True` the preference value will be read
+ from the default branch. Otherwise the user-defined
+ value if set is returned. Defaults to `False`.
+ :param value_type: Optional, XPCOM interface of the pref's complex value.
+ Possible values are: `nsIFile` and
+ `nsIPrefLocalizedString`.
+
+ Usage example::
+
+ marionette.get_pref("browser.tabs.warnOnClose")
+
+ """
+ with self.using_context(self.CONTEXT_CHROME):
+ pref_value = self.execute_script(
+ """
+ const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+ );
+
+ let pref = arguments[0];
+ let defaultBranch = arguments[1];
+ let valueType = arguments[2];
+
+ prefs = new Preferences({defaultBranch: defaultBranch});
+ return prefs.get(pref, null, Components.interfaces[valueType]);
+ """,
+ script_args=(pref, default_branch, value_type),
+ )
+ return pref_value
+
+ def set_pref(self, pref, value, default_branch=False):
+ """Set the value of the specified preference.
+
+ :param pref: Name of the preference.
+ :param value: The value to set the preference to. If the value is None,
+ reset the preference to its default value. If no default
+ value exists, the preference will cease to exist.
+ :param default_branch: Optional, if `True` the preference value will
+ be written to the default branch, and will remain until
+ the application gets restarted. Otherwise a user-defined
+ value is set. Defaults to `False`.
+
+ Usage example::
+
+ marionette.set_pref("browser.tabs.warnOnClose", True)
+
+ """
+ with self.using_context(self.CONTEXT_CHROME):
+ if value is None:
+ self.clear_pref(pref)
+ return
+
+ self.execute_script(
+ """
+ const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+ );
+
+ let pref = arguments[0];
+ let value = arguments[1];
+ let defaultBranch = arguments[2];
+
+ prefs = new Preferences({defaultBranch: defaultBranch});
+ prefs.set(pref, value);
+ """,
+ script_args=(pref, value, default_branch),
+ )
+
+ def set_prefs(self, prefs, default_branch=False):
+ """Set the value of a list of preferences.
+
+ :param prefs: A dict containing one or more preferences and their values
+ to be set. See :func:`set_pref` for further details.
+ :param default_branch: Optional, if `True` the preference value will
+ be written to the default branch, and will remain until
+ the application gets restarted. Otherwise a user-defined
+ value is set. Defaults to `False`.
+
+ Usage example::
+
+ marionette.set_prefs({"browser.tabs.warnOnClose": True})
+
+ """
+ for pref, value in prefs.items():
+ self.set_pref(pref, value, default_branch=default_branch)
+
+ @contextmanager
+ def using_prefs(self, prefs, default_branch=False):
+ """Set preferences for code executed in a `with` block, and restores them on exit.
+
+ :param prefs: A dict containing one or more preferences and their values
+ to be set. See :func:`set_prefs` for further details.
+ :param default_branch: Optional, if `True` the preference value will
+ be written to the default branch, and will remain until
+ the application gets restarted. Otherwise a user-defined
+ value is set. Defaults to `False`.
+
+ Usage example::
+
+ with marionette.using_prefs({"browser.tabs.warnOnClose": True}):
+ # ... do stuff ...
+
+ """
+ original_prefs = {p: self.get_pref(p) for p in prefs}
+ self.set_prefs(prefs, default_branch=default_branch)
+
+ try:
+ yield
+ finally:
+ self.set_prefs(original_prefs, default_branch=default_branch)
+
+ @do_process_check
+ def enforce_gecko_prefs(self, prefs):
+ """Checks if the running instance has the given prefs. If not,
+ it will kill the currently running instance, and spawn a new
+ instance with the requested preferences.
+
+ :param prefs: A dictionary whose keys are preference names.
+ """
+ if not self.instance:
+ raise errors.MarionetteException(
+ "enforce_gecko_prefs() can only be called "
+ "on Gecko instances launched by Marionette"
+ )
+ pref_exists = True
+ with self.using_context(self.CONTEXT_CHROME):
+ for pref, value in six.iteritems(prefs):
+ if type(value) is not str:
+ value = json.dumps(value)
+ pref_exists = self.execute_script(
+ """
+ let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ let pref = '{0}';
+ let value = '{1}';
+ let type = prefInterface.getPrefType(pref);
+ switch(type) {{
+ case prefInterface.PREF_STRING:
+ return value == prefInterface.getCharPref(pref).toString();
+ case prefInterface.PREF_BOOL:
+ return value == prefInterface.getBoolPref(pref).toString();
+ case prefInterface.PREF_INT:
+ return value == prefInterface.getIntPref(pref).toString();
+ case prefInterface.PREF_INVALID:
+ return false;
+ }}
+ """.format(
+ pref, value
+ )
+ )
+ if not pref_exists:
+ break
+
+ if not pref_exists:
+ context = self._send_message("Marionette:GetContext", key="value")
+ self.delete_session()
+ self.instance.restart(prefs)
+ self.raise_for_port()
+ self.start_session(self.requested_capabilities)
+
+ # Restore the context as used before the restart
+ self.set_context(context)
+
+ def _request_in_app_shutdown(self, flags=None, safe_mode=False):
+ """Attempt to quit the currently running instance from inside the
+ application. If shutdown is prevented by some component the quit
+ will be forced.
+
+ This method effectively calls `Services.startup.quit` in Gecko.
+ Possible flag values are listed at https://bit.ly/3IYcjYi.
+
+ :param flags: Optional additional quit masks to include.
+
+ :param safe_mode: Optional flag to indicate that the application has to
+ be restarted in safe mode.
+
+ :returns: A dictionary containing details of the application shutdown.
+ The `cause` property reflects the reason, and `forced` indicates
+ that something prevented the shutdown and the application had
+ to be forced to shutdown.
+
+ :throws InvalidArgumentException: If there are multiple
+ `shutdown_flags` ending with `"Quit"`.
+ """
+ body = {}
+ if flags is not None:
+ body["flags"] = list(
+ flags,
+ )
+ if safe_mode:
+ body["safeMode"] = safe_mode
+
+ return self._send_message("Marionette:Quit", body)
+
+ @do_process_check
+ def quit(self, clean=False, in_app=True, callback=None):
+ """
+ By default this method will trigger a normal shutdown of the currently running instance.
+ But it can also be used to force terminate the process.
+
+ This command will delete the active marionette session. It also allows
+ manipulation of eg. the profile data while the application is not running.
+ To start the application again, :func:`start_session` has to be called.
+
+ :param clean: If True a new profile will be used after the next start of
+ the application. Note that the in_app initiated quit always
+ maintains the same profile.
+
+ :param in_app: If True, marionette will cause a quit from within the
+ application. Otherwise the application will be restarted
+ immediately by killing the process.
+
+ :param callback: If provided and `in_app` is True, the callback will
+ be used to trigger the shutdown.
+
+ :returns: A dictionary containing details of the application shutdown.
+ The `cause` property reflects the reason, and `forced` indicates
+ that something prevented the shutdown and the application had
+ to be forced to shutdown.
+ """
+ if not self.instance:
+ raise errors.MarionetteException(
+ "quit() can only be called " "on Gecko instances launched by Marionette"
+ )
+
+ quit_details = {"cause": "shutdown", "forced": False}
+
+ if in_app:
+ if clean:
+ raise ValueError(
+ "An in_app restart cannot be triggered with the clean flag set"
+ )
+
+ if callback is not None and not callable(callback):
+ raise ValueError(
+ "Specified callback '{}' is not callable".format(callback)
+ )
+
+ # Block Marionette from accepting new connections
+ self._send_message("Marionette:AcceptConnections", {"value": False})
+
+ try:
+ self.is_shutting_down = True
+ if callback is not None:
+ callback()
+ quit_details["in_app"] = True
+ else:
+ quit_details = self._request_in_app_shutdown()
+
+ except IOError:
+ # A possible IOError should be ignored at this point, given that
+ # quit() could have been called inside of `using_context`,
+ # which wants to reset the context but fails sending the message.
+ pass
+
+ except Exception:
+ # For any other error assume the application is not going to shutdown.
+ # As such allow Marionette to accept new connections again.
+ self.is_shutting_down = False
+ self._send_message("Marionette:AcceptConnections", {"value": True})
+ raise
+
+ try:
+ self.delete_session(send_request=False)
+
+ # Try to wait for the process to end itself before force-closing it.
+ returncode = self.instance.runner.wait(timeout=self.shutdown_timeout)
+ if returncode is None:
+ self.cleanup()
+
+ message = "Process still running {}s after quit request"
+ raise IOError(message.format(self.shutdown_timeout))
+
+ finally:
+ self.is_shutting_down = False
+
+ else:
+ self.delete_session(send_request=False)
+ self.instance.close(clean=clean)
+
+ quit_details.update({"in_app": False, "forced": True})
+
+ if quit_details.get("cause") not in (None, "shutdown"):
+ raise errors.MarionetteException(
+ "Unexpected shutdown reason '{}' for "
+ "quitting the process.".format(quit_details["cause"])
+ )
+
+ return quit_details
+
+ @do_process_check
+ def restart(
+ self, callback=None, clean=False, in_app=True, safe_mode=False, silent=False
+ ):
+ """
+ By default this method will restart the currently running instance by using the same
+ profile. But it can also be forced to terminate the currently running instance, and
+ to spawn a new instance with the same or different profile.
+
+ :param callback: If provided and `in_app` is True, the callback will be
+ used to trigger the restart.
+
+ :param clean: If True a new profile will be used after the restart. Note
+ that the in_app initiated restart always maintains the same
+ profile.
+
+ :param in_app: If True, marionette will cause a restart from within the
+ application. Otherwise the application will be restarted
+ immediately by killing the process.
+
+ :param safe_mode: Optional flag to indicate that the application has to
+ be restarted in safe mode.
+
+ :param silent: Optional flag to indicate that the application should
+ not open any window after a restart. Note that this flag is only
+ supported on MacOS and requires "in_app" to be True.
+
+ :returns: A dictionary containing details of the application restart.
+ The `cause` property reflects the reason, and `forced` indicates
+ that something prevented the shutdown and the application had
+ to be forced to shutdown.
+ """
+ if not self.instance:
+ raise errors.MarionetteException(
+ "restart() can only be called "
+ "on Gecko instances launched by Marionette"
+ )
+
+ context = self._send_message("Marionette:GetContext", key="value")
+ restart_details = {"cause": "restart", "forced": False}
+
+ # Safe mode and the silent flag require an in_app restart.
+ if (safe_mode or silent) and not in_app:
+ raise ValueError("An in_app restart is required for safe or silent mode")
+
+ if in_app:
+ if clean:
+ raise ValueError(
+ "An in_app restart cannot be triggered with the clean flag set"
+ )
+
+ if callback is not None and not callable(callback):
+ raise ValueError(
+ "Specified callback '{}' is not callable".format(callback)
+ )
+
+ # Block Marionette from accepting new connections
+ self._send_message("Marionette:AcceptConnections", {"value": False})
+
+ try:
+ self.is_shutting_down = True
+ if callback is not None:
+ callback()
+ restart_details["in_app"] = True
+ else:
+ flags = ["eRestart"]
+ if silent:
+ flags.append("eSilently")
+
+ try:
+ restart_details = self._request_in_app_shutdown(
+ flags=flags, safe_mode=safe_mode
+ )
+ except Exception as e:
+ self._send_message(
+ "Marionette:AcceptConnections", {"value": True}
+ )
+ raise e
+
+ except IOError:
+ # A possible IOError should be ignored at this point, given that
+ # restart() could have been called inside of `using_context`,
+ # which wants to reset the context but fails sending the message.
+ pass
+
+ timeout_restart = self.shutdown_timeout + self.startup_timeout
+ try:
+ # Wait for a new Marionette connection to appear while the
+ # process restarts itself.
+ self.raise_for_port(timeout=timeout_restart, check_process_status=False)
+ except socket.timeout:
+ exc_cls, _, tb = sys.exc_info()
+
+ if self.instance.runner.returncode is None:
+ # The process is still running, which means the shutdown
+ # request was not correct or the application ignored it.
+ # Allow Marionette to accept connections again.
+ self._send_message("Marionette:AcceptConnections", {"value": True})
+
+ message = "Process still running {}s after restart request"
+ reraise(exc_cls, exc_cls(message.format(timeout_restart)), tb)
+
+ else:
+ # The process shutdown but didn't start again.
+ self.cleanup()
+ msg = "Process unexpectedly quit without restarting (exit code: {})"
+ reraise(
+ exc_cls,
+ exc_cls(msg.format(self.instance.runner.returncode)),
+ tb,
+ )
+
+ finally:
+ self.is_shutting_down = False
+
+ self.delete_session(send_request=False)
+
+ else:
+ self.delete_session()
+ self.instance.restart(clean=clean)
+ self.raise_for_port(timeout=self.DEFAULT_STARTUP_TIMEOUT)
+
+ restart_details.update({"in_app": False, "forced": True})
+
+ if restart_details.get("cause") not in (None, "restart"):
+ raise errors.MarionetteException(
+ "Unexpected shutdown reason '{}' for "
+ "restarting the process".format(restart_details["cause"])
+ )
+
+ self.start_session(self.requested_capabilities)
+ # Restore the context as used before the restart
+ self.set_context(context)
+
+ if in_app and self.process_id:
+ # In some cases Firefox restarts itself by spawning into a new process group.
+ # As long as mozprocess cannot track that behavior (bug 1284864) we assist by
+ # informing about the new process id.
+ self.instance.runner.process_handler.check_for_detached(self.process_id)
+
+ return restart_details
+
+ def absolute_url(self, relative_url):
+ """
+ Returns an absolute url for files served from Marionette's www directory.
+
+ :param relative_url: The url of a static file, relative to Marionette's www directory.
+ """
+ return "{0}{1}".format(self.baseurl, relative_url)
+
+ @do_process_check
+ def start_session(self, capabilities=None, timeout=None):
+ """Create a new WebDriver session.
+ This method must be called before performing any other action.
+
+ :param capabilities: An optional dictionary of
+ Marionette-recognised capabilities. It does not
+ accept a WebDriver conforming capabilities dictionary
+ (including alwaysMatch, firstMatch, desiredCapabilities,
+ or requriedCapabilities), and only recognises extension
+ capabilities that are specific to Marionette.
+ :param timeout: Optional timeout in seconds for the server to be ready.
+ :returns: A dictionary of the capabilities offered.
+ """
+ if capabilities is None:
+ capabilities = {"strictFileInteractability": True}
+ self.requested_capabilities = capabilities
+
+ if timeout is None:
+ timeout = self.startup_timeout
+
+ self.crashed = 0
+
+ if self.instance:
+ returncode = self.instance.runner.returncode
+ # We're managing a binary which has terminated. Start it again
+ # and implicitely wait for the Marionette server to be ready.
+ if returncode is not None:
+ self.start_binary(timeout)
+
+ else:
+ # In the case when Marionette doesn't manage the binary wait until
+ # its server component has been started.
+ self.raise_for_port(timeout=timeout)
+
+ self.client = transport.TcpTransport(self.host, self.port, self.socket_timeout)
+ self.protocol, _ = self.client.connect()
+
+ try:
+ resp = self._send_message("WebDriver:NewSession", capabilities)
+ except errors.UnknownException:
+ # Force closing the managed process when the session cannot be
+ # created due to global JavaScript errors.
+ exc_type, value, tb = sys.exc_info()
+ if self.instance and self.instance.runner.is_running():
+ self.instance.close()
+ reraise(exc_type, exc_type(value.message), tb)
+
+ self.session_id = resp["sessionId"]
+ self.session = resp["capabilities"]
+ self.cleanup_ran = False
+ # fallback to processId can be removed in Firefox 55
+ self.process_id = self.session.get(
+ "moz:processID", self.session.get("processId")
+ )
+ self.profile = self.session.get("moz:profile")
+
+ timeout = self.session.get("moz:shutdownTimeout")
+ if timeout is not None:
+ # pylint --py3k W1619
+ self.shutdown_timeout = timeout / 1000 + 10
+
+ return self.session
+
+ @property
+ def test_name(self):
+ return self._test_name
+
+ @test_name.setter
+ def test_name(self, test_name):
+ self._test_name = test_name
+
+ def delete_session(self, send_request=True):
+ """Close the current session and disconnect from the server.
+
+ :param send_request: Optional, if `True` a request to close the session on
+ the server side will be sent. Use `False` in case of eg. in_app restart()
+ or quit(), which trigger a deletion themselves. Defaults to `True`.
+ """
+ try:
+ if send_request:
+ try:
+ self._send_message("WebDriver:DeleteSession")
+ except errors.InvalidSessionIdException:
+ pass
+ finally:
+ self.process_id = None
+ self.profile = None
+ self.session = None
+ self.session_id = None
+ self.window = None
+
+ if self.client is not None:
+ self.client.close()
+
+ @property
+ def session_capabilities(self):
+ """A JSON dictionary representing the capabilities of the
+ current session.
+
+ """
+ return self.session
+
+ @property
+ def current_window_handle(self):
+ """Get the current window's handle.
+
+ Returns an opaque server-assigned identifier to this window
+ that uniquely identifies it within this Marionette instance.
+ This can be used to switch to this window at a later point.
+
+ :returns: unique window handle
+ :rtype: string
+ """
+ with self.using_context("content"):
+ self.window = self._send_message("WebDriver:GetWindowHandle", key="value")
+
+ return self.window
+
+ @property
+ def current_chrome_window_handle(self):
+ """Get the current chrome window's handle. Corresponds to
+ a chrome window that may itself contain tabs identified by
+ window_handles.
+
+ Returns an opaque server-assigned identifier to this window
+ that uniquely identifies it within this Marionette instance.
+ This can be used to switch to this window at a later point.
+
+ :returns: unique window handle
+ :rtype: string
+ """
+ with self.using_context("chrome"):
+ self.chrome_window = self._send_message(
+ "WebDriver:GetWindowHandle", key="value"
+ )
+
+ return self.chrome_window
+
+ def set_window_rect(self, x=None, y=None, height=None, width=None):
+ """Set the position and size of the current window.
+
+ The supplied width and height values refer to the window outerWidth
+ and outerHeight values, which include scroll bars, title bars, etc.
+
+ An error will be returned if the requested window size would result
+ in the window being in the maximised state.
+
+ :param x: x coordinate for the top left of the window
+ :param y: y coordinate for the top left of the window
+ :param width: The width to resize the window to.
+ :param height: The height to resize the window to.
+ """
+ if (x is None and y is None) and (height is None and width is None):
+ raise errors.InvalidArgumentException(
+ "x and y or height and width need values"
+ )
+
+ body = {"x": x, "y": y, "height": height, "width": width}
+ return self._send_message("WebDriver:SetWindowRect", body)
+
+ @property
+ def window_rect(self):
+ return self._send_message("WebDriver:GetWindowRect")
+
+ @property
+ def title(self):
+ """Current title of the active window."""
+ return self._send_message("WebDriver:GetTitle", key="value")
+
+ @property
+ def window_handles(self):
+ """Get list of windows in the current context.
+
+ If called in the content context it will return a list of
+ references to all available browser windows.
+
+ Each window handle is assigned by the server, and the list of
+ strings returned does not have a guaranteed ordering.
+
+ :returns: Unordered list of unique window handles as strings
+ """
+ with self.using_context("content"):
+ return self._send_message("WebDriver:GetWindowHandles")
+
+ @property
+ def chrome_window_handles(self):
+ """Get a list of currently open chrome windows.
+
+ Each window handle is assigned by the server, and the list of
+ strings returned does not have a guaranteed ordering.
+
+ :returns: Unordered list of unique chrome window handles as strings
+ """
+ with self.using_context("chrome"):
+ return self._send_message("WebDriver:GetWindowHandles")
+
+ @property
+ def page_source(self):
+ """A string representation of the DOM."""
+ return self._send_message("WebDriver:GetPageSource", key="value")
+
+ def open(self, type=None, focus=False, private=False):
+ """Open a new window, or tab based on the specified context type.
+
+ If no context type is given the application will choose the best
+ option based on tab and window support.
+
+ :param type: Type of window to be opened. Can be one of "tab" or "window"
+ :param focus: If true, the opened window will be focused
+ :param private: If true, open a private window
+
+ :returns: Dict with new window handle, and type of opened window
+ """
+ body = {"type": type, "focus": focus, "private": private}
+ return self._send_message("WebDriver:NewWindow", body)
+
+ def close(self):
+ """Close the current window, ending the session if it's the last
+ window currently open.
+
+ :returns: Unordered list of remaining unique window handles as strings
+ """
+ return self._send_message("WebDriver:CloseWindow")
+
+ def close_chrome_window(self):
+ """Close the currently selected chrome window, ending the session
+ if it's the last window open.
+
+ :returns: Unordered list of remaining unique chrome window handles as strings
+ """
+ return self._send_message("WebDriver:CloseChromeWindow")
+
+ def set_context(self, context):
+ """Sets the context that Marionette commands are running in.
+
+ :param context: Context, may be one of the class properties
+ `CONTEXT_CHROME` or `CONTEXT_CONTENT`.
+
+ Usage example::
+
+ marionette.set_context(marionette.CONTEXT_CHROME)
+ """
+ if context not in [self.CONTEXT_CHROME, self.CONTEXT_CONTENT]:
+ raise ValueError("Unknown context: {}".format(context))
+
+ self._send_message("Marionette:SetContext", {"value": context})
+
+ @contextmanager
+ def using_context(self, context):
+ """Sets the context that Marionette commands are running in using
+ a `with` statement. The state of the context on the server is
+ saved before entering the block, and restored upon exiting it.
+
+ :param context: Context, may be one of the class properties
+ `CONTEXT_CHROME` or `CONTEXT_CONTENT`.
+
+ Usage example::
+
+ with marionette.using_context(marionette.CONTEXT_CHROME):
+ # chrome scope
+ ... do stuff ...
+ """
+ scope = self._send_message("Marionette:GetContext", key="value")
+ self.set_context(context)
+ try:
+ yield
+ finally:
+ self.set_context(scope)
+
+ def switch_to_alert(self):
+ """Returns an :class:`~marionette_driver.marionette.Alert` object for
+ interacting with a currently displayed alert.
+
+ ::
+
+ alert = self.marionette.switch_to_alert()
+ text = alert.text
+ alert.accept()
+ """
+ return Alert(self)
+
+ def switch_to_window(self, handle, focus=True):
+ """Switch to the specified window; subsequent commands will be
+ directed at the new window.
+
+ :param handle: The id of the window to switch to.
+
+ :param focus: A boolean value which determins whether to focus
+ the window that we just switched to.
+ """
+ self._send_message(
+ "WebDriver:SwitchToWindow", {"handle": handle, "focus": focus}
+ )
+ self.window = handle
+
+ def switch_to_default_content(self):
+ """Switch the current context to page's default content."""
+ return self.switch_to_frame()
+
+ def switch_to_parent_frame(self):
+ """
+ Switch to the Parent Frame
+ """
+ self._send_message("WebDriver:SwitchToParentFrame")
+
+ def switch_to_frame(self, frame=None):
+ """Switch the current context to the specified frame. Subsequent
+ commands will operate in the context of the specified frame,
+ if applicable.
+
+ :param frame: A reference to the frame to switch to. This can
+ be an :class:`~marionette_driver.marionette.WebElement`,
+ or an integer index. If you call ``switch_to_frame`` without an
+ argument, it will switch to the top-level frame.
+ """
+ body = {}
+ if isinstance(frame, WebElement):
+ body["element"] = frame.id
+ elif frame is not None:
+ body["id"] = frame
+
+ self._send_message("WebDriver:SwitchToFrame", body)
+
+ def get_url(self):
+ """Get a string representing the current URL.
+
+ On Desktop this returns a string representation of the URL of
+ the current top level browsing context. This is equivalent to
+ document.location.href.
+
+ When in the context of the chrome, this returns the canonical
+ URL of the current resource.
+
+ :returns: string representation of URL
+ """
+ return self._send_message("WebDriver:GetCurrentURL", key="value")
+
+ def get_window_type(self):
+ """Gets the windowtype attribute of the window Marionette is
+ currently acting on.
+
+ This command only makes sense in a chrome context. You might use this
+ method to distinguish a browser window from an editor window.
+ """
+ try:
+ return self._send_message("Marionette:GetWindowType", key="value")
+ except errors.UnknownCommandException:
+ return self._send_message("getWindowType", key="value")
+
+ def navigate(self, url):
+ """Navigate to given `url`.
+
+ Navigates the current top-level browsing context's content
+ frame to the given URL and waits for the document to load or
+ the session's page timeout duration to elapse before returning.
+
+ The command will return with a failure if there is an error
+ loading the document or the URL is blocked. This can occur if
+ it fails to reach the host, the URL is malformed, the page is
+ restricted (about:* pages), or if there is a certificate issue
+ to name some examples.
+
+ The document is considered successfully loaded when the
+ `DOMContentLoaded` event on the frame element associated with the
+ `window` triggers and `document.readyState` is "complete".
+
+ In chrome context it will change the current `window`'s location
+ to the supplied URL and wait until `document.readyState` equals
+ "complete" or the page timeout duration has elapsed.
+
+ :param url: The URL to navigate to.
+ """
+ self._send_message("WebDriver:Navigate", {"url": url})
+
+ def go_back(self):
+ """Causes the browser to perform a back navigation."""
+ self._send_message("WebDriver:Back")
+
+ def go_forward(self):
+ """Causes the browser to perform a forward navigation."""
+ self._send_message("WebDriver:Forward")
+
+ def refresh(self):
+ """Causes the browser to perform to refresh the current page."""
+ self._send_message("WebDriver:Refresh")
+
+ def _to_json(self, args):
+ if isinstance(args, (list, tuple)):
+ wrapped = []
+ for arg in args:
+ wrapped.append(self._to_json(arg))
+ elif isinstance(args, dict):
+ wrapped = {}
+ for arg in args:
+ wrapped[arg] = self._to_json(args[arg])
+ elif type(args) == WebElement:
+ wrapped = {WEB_ELEMENT_KEY: args.id}
+ elif type(args) == ShadowRoot:
+ wrapped = {WEB_SHADOW_ROOT_KEY: args.id}
+ elif type(args) == WebFrame:
+ wrapped = {WEB_FRAME_KEY: args.id}
+ elif type(args) == WebWindow:
+ wrapped = {WEB_WINDOW_KEY: args.id}
+ elif isinstance(args, (bool, int, float, six.string_types)) or args is None:
+ wrapped = args
+ return wrapped
+
+ def _from_json(self, value):
+ if isinstance(value, dict) and any(
+ k in value.keys() for k in WebElement.identifiers
+ ):
+ return WebElement._from_json(value, self)
+ elif isinstance(value, dict) and any(
+ k in value.keys() for k in ShadowRoot.identifiers
+ ):
+ return ShadowRoot._from_json(value, self)
+ elif isinstance(value, dict) and any(
+ k in value.keys() for k in WebFrame.identifiers
+ ):
+ return WebFrame._from_json(value, self)
+ elif isinstance(value, dict) and any(
+ k in value.keys() for k in WebWindow.identifiers
+ ):
+ return WebWindow._from_json(value, self)
+ elif isinstance(value, dict):
+ return {key: self._from_json(val) for key, val in value.items()}
+ elif isinstance(value, list):
+ return list(self._from_json(item) for item in value)
+ else:
+ return value
+
+ def execute_script(
+ self,
+ script,
+ script_args=(),
+ new_sandbox=True,
+ sandbox="default",
+ script_timeout=None,
+ ):
+ """Executes a synchronous JavaScript script, and returns the
+ result (or None if the script does return a value).
+
+ The script is executed in the context set by the most recent
+ :func:`set_context` call, or to the CONTEXT_CONTENT context if
+ :func:`set_context` has not been called.
+
+ :param script: A string containing the JavaScript to execute.
+ :param script_args: An interable of arguments to pass to the script.
+ :param new_sandbox: If False, preserve global variables from
+ the last execute_*script call. This is True by default, in which
+ case no globals are preserved.
+ :param sandbox: A tag referring to the sandbox you wish to use;
+ if you specify a new tag, a new sandbox will be created.
+ If you use the special tag `system`, the sandbox will
+ be created using the system principal which has elevated
+ privileges.
+ :param script_timeout: Timeout in milliseconds, overriding
+ the session's default script timeout.
+
+ Simple usage example:
+
+ ::
+
+ result = marionette.execute_script("return 1;")
+ assert result == 1
+
+ You can use the `script_args` parameter to pass arguments to the
+ script:
+
+ ::
+
+ result = marionette.execute_script("return arguments[0] + arguments[1];",
+ script_args=(2, 3,))
+ assert result == 5
+ some_element = marionette.find_element(By.ID, "someElement")
+ sid = marionette.execute_script("return arguments[0].id;", script_args=(some_element,))
+ assert some_element.get_attribute("id") == sid
+
+ Scripts wishing to access non-standard properties of the window
+ object must use window.wrappedJSObject:
+
+ ::
+
+ result = marionette.execute_script('''
+ window.wrappedJSObject.test1 = "foo";
+ window.wrappedJSObject.test2 = "bar";
+ return window.wrappedJSObject.test1 + window.wrappedJSObject.test2;
+ ''')
+ assert result == "foobar"
+
+ Global variables set by individual scripts do not persist between
+ script calls by default. If you wish to persist data between
+ script calls, you can set `new_sandbox` to False on your next call,
+ and add any new variables to a new 'global' object like this:
+
+ ::
+
+ marionette.execute_script("global.test1 = 'foo';")
+ result = self.marionette.execute_script("return global.test1;", new_sandbox=False)
+ assert result == "foo"
+
+ """
+ original_timeout = None
+ if script_timeout is not None:
+ original_timeout = self.timeout.script
+ self.timeout.script = script_timeout / 1000.0
+
+ try:
+ args = self._to_json(script_args)
+ stack = traceback.extract_stack()
+ frame = stack[-2:-1][0] # grab the second-to-last frame
+ filename = (
+ frame[0] if sys.platform == "win32" else os.path.relpath(frame[0])
+ )
+ body = {
+ "script": script.strip(),
+ "args": args,
+ "newSandbox": new_sandbox,
+ "sandbox": sandbox,
+ "line": int(frame[1]),
+ "filename": filename,
+ }
+ rv = self._send_message("WebDriver:ExecuteScript", body, key="value")
+
+ finally:
+ if script_timeout is not None:
+ self.timeout.script = original_timeout
+
+ return rv
+
+ def execute_async_script(
+ self,
+ script,
+ script_args=(),
+ new_sandbox=True,
+ sandbox="default",
+ script_timeout=None,
+ ):
+ """Executes an asynchronous JavaScript script, and returns the
+ result (or None if the script does return a value).
+
+ The script is executed in the context set by the most recent
+ :func:`set_context` call, or to the CONTEXT_CONTENT context if
+ :func:`set_context` has not been called.
+
+ :param script: A string containing the JavaScript to execute.
+ :param script_args: An interable of arguments to pass to the script.
+ :param new_sandbox: If False, preserve global variables from
+ the last execute_*script call. This is True by default,
+ in which case no globals are preserved.
+ :param sandbox: A tag referring to the sandbox you wish to use; if
+ you specify a new tag, a new sandbox will be created. If you
+ use the special tag `system`, the sandbox will be created
+ using the system principal which has elevated privileges.
+ :param script_timeout: Timeout in milliseconds, overriding
+ the session's default script timeout.
+
+ Usage example:
+
+ ::
+
+ marionette.timeout.script = 10
+ result = self.marionette.execute_async_script('''
+ // this script waits 5 seconds, and then returns the number 1
+ let [resolve] = arguments;
+ setTimeout(function() {
+ resolve(1);
+ }, 5000);
+ ''')
+ assert result == 1
+ """
+ original_timeout = None
+ if script_timeout is not None:
+ original_timeout = self.timeout.script
+ self.timeout.script = script_timeout / 1000.0
+
+ try:
+ args = self._to_json(script_args)
+ stack = traceback.extract_stack()
+ frame = stack[-2:-1][0] # grab the second-to-last frame
+ filename = (
+ frame[0] if sys.platform == "win32" else os.path.relpath(frame[0])
+ )
+ body = {
+ "script": script.strip(),
+ "args": args,
+ "newSandbox": new_sandbox,
+ "sandbox": sandbox,
+ "scriptTimeout": script_timeout,
+ "line": int(frame[1]),
+ "filename": filename,
+ }
+ rv = self._send_message("WebDriver:ExecuteAsyncScript", body, key="value")
+
+ finally:
+ if script_timeout is not None:
+ self.timeout.script = original_timeout
+
+ return rv
+
+ def find_element(self, method, target, id=None):
+ """Returns an :class:`~marionette_driver.marionette.WebElement`
+ instance that matches the specified method and target in the current
+ context.
+
+ An :class:`~marionette_driver.marionette.WebElement` instance may be
+ used to call other methods on the element, such as
+ :func:`~marionette_driver.marionette.WebElement.click`. If no element
+ is immediately found, the attempt to locate an element will be repeated
+ for up to the amount of time set by
+ :attr:`marionette_driver.timeout.Timeouts.implicit`. If multiple
+ elements match the given criteria, only the first is returned. If no
+ element matches, a ``NoSuchElementException`` will be raised.
+
+ :param method: The method to use to locate the element; one of:
+ "id", "name", "class name", "tag name", "css selector",
+ "link text", "partial link text" and "xpath".
+ Note that the "name", "link text" and "partial link test"
+ methods are not supported in the chrome DOM.
+ :param target: The target of the search. For example, if method =
+ "tag", target might equal "div". If method = "id", target would
+ be an element id.
+ :param id: If specified, search for elements only inside the element
+ with the specified id.
+ """
+ body = {"value": target, "using": method}
+ if id:
+ body["element"] = id
+
+ return self._send_message("WebDriver:FindElement", body, key="value")
+
+ def find_elements(self, method, target, id=None):
+ """Returns a list of all
+ :class:`~marionette_driver.marionette.WebElement` instances that match
+ the specified method and target in the current context.
+
+ An :class:`~marionette_driver.marionette.WebElement` instance may be
+ used to call other methods on the element, such as
+ :func:`~marionette_driver.marionette.WebElement.click`. If no element
+ is immediately found, the attempt to locate an element will be repeated
+ for up to the amount of time set by
+ :attr:`marionette_driver.timeout.Timeouts.implicit`.
+
+ :param method: The method to use to locate the elements; one
+ of: "id", "name", "class name", "tag name", "css selector",
+ "link text", "partial link text" and "xpath".
+ Note that the "name", "link text" and "partial link test"
+ methods are not supported in the chrome DOM.
+ :param target: The target of the search. For example, if method =
+ "tag", target might equal "div". If method = "id", target would be
+ an element id.
+ :param id: If specified, search for elements only inside the element
+ with the specified id.
+ """
+ body = {"value": target, "using": method}
+ if id:
+ body["element"] = id
+
+ return self._send_message("WebDriver:FindElements", body)
+
+ def get_active_element(self):
+ el_or_ref = self._send_message("WebDriver:GetActiveElement", key="value")
+ return el_or_ref
+
+ def add_cookie(self, cookie):
+ """Adds a cookie to your current session.
+
+ :param cookie: A dictionary object, with required keys - "name"
+ and "value"; optional keys - "path", "domain", "secure",
+ "expiry".
+
+ Usage example:
+
+ ::
+
+ driver.add_cookie({"name": "foo", "value": "bar"})
+ driver.add_cookie({"name": "foo", "value": "bar", "path": "/"})
+ driver.add_cookie({"name": "foo", "value": "bar", "path": "/",
+ "secure": True})
+ """
+ self._send_message("WebDriver:AddCookie", {"cookie": cookie})
+
+ def delete_all_cookies(self):
+ """Delete all cookies in the scope of the current session.
+
+ Usage example:
+
+ ::
+
+ driver.delete_all_cookies()
+ """
+ self._send_message("WebDriver:DeleteAllCookies")
+
+ def delete_cookie(self, name):
+ """Delete a cookie by its name.
+
+ :param name: Name of cookie to delete.
+
+ Usage example:
+
+ ::
+
+ driver.delete_cookie("foo")
+ """
+ self._send_message("WebDriver:DeleteCookie", {"name": name})
+
+ def get_cookie(self, name):
+ """Get a single cookie by name. Returns the cookie if found,
+ None if not.
+
+ :param name: Name of cookie to get.
+ """
+ cookies = self.get_cookies()
+ for cookie in cookies:
+ if cookie["name"] == name:
+ return cookie
+ return None
+
+ def get_cookies(self):
+ """Get all the cookies for the current domain.
+
+ This is the equivalent of calling `document.cookie` and
+ parsing the result.
+
+ :returns: A list of cookies for the current domain.
+ """
+ return self._send_message("WebDriver:GetCookies")
+
+ def save_screenshot(self, fh, element=None, full=True, scroll=True):
+ """Takes a screenhot of a web element or the current frame and
+ saves it in the filehandle.
+
+ It is a wrapper around screenshot()
+ :param fh: The filehandle to save the screenshot at.
+
+ The rest of the parameters are defined like in screenshot()
+ """
+ data = self.screenshot(element, "binary", full, scroll)
+ fh.write(data)
+
+ def screenshot(self, element=None, format="base64", full=True, scroll=True):
+ """Takes a screenshot of a web element or the current frame.
+
+ The screen capture is returned as a lossless PNG image encoded
+ as a base 64 string by default. If the `element` argument is defined the
+ capture area will be limited to the bounding box of that
+ element. Otherwise, the capture area will be the bounding box
+ of the current frame.
+
+ :param element: The element to take a screenshot of. If None, will
+ take a screenshot of the current frame.
+
+ :param format: if "base64" (the default), returns the screenshot
+ as a base64-string. If "binary", the data is decoded and
+ returned as raw binary. If "hash", the data is hashed using
+ the SHA-256 algorithm and the result is returned as a hex digest.
+
+ :param full: If True (the default), the capture area will be the
+ complete frame. Else only the viewport is captured. Only applies
+ when `element` is None.
+
+ :param scroll: When `element` is provided, scroll to it before
+ taking the screenshot (default). Otherwise, avoid scrolling
+ `element` into view.
+ """
+
+ if element:
+ element = element.id
+
+ body = {"id": element, "full": full, "hash": False, "scroll": scroll}
+ if format == "hash":
+ body["hash"] = True
+
+ data = self._send_message("WebDriver:TakeScreenshot", body, key="value")
+
+ if format == "base64" or format == "hash":
+ return data
+ elif format == "binary":
+ return base64.b64decode(data.encode("ascii"))
+ else:
+ raise ValueError(
+ "format parameter must be either 'base64'"
+ " or 'binary', not {0}".format(repr(format))
+ )
+
+ @property
+ def orientation(self):
+ """Get the current browser orientation.
+
+ Will return one of the valid primary orientation values
+ portrait-primary, landscape-primary, portrait-secondary, or
+ landscape-secondary.
+ """
+ try:
+ return self._send_message("Marionette:GetScreenOrientation", key="value")
+ except errors.UnknownCommandException:
+ return self._send_message("getScreenOrientation", key="value")
+
+ def set_orientation(self, orientation):
+ """Set the current browser orientation.
+
+ The supplied orientation should be given as one of the valid
+ orientation values. If the orientation is unknown, an error
+ will be raised.
+
+ Valid orientations are "portrait" and "landscape", which fall
+ back to "portrait-primary" and "landscape-primary"
+ respectively, and "portrait-secondary" as well as
+ "landscape-secondary".
+
+ :param orientation: The orientation to lock the screen in.
+ """
+ body = {"orientation": orientation}
+ try:
+ self._send_message("Marionette:SetScreenOrientation", body)
+ except errors.UnknownCommandException:
+ self._send_message("setScreenOrientation", body)
+
+ def minimize_window(self):
+ """Iconify the browser window currently receiving commands.
+ The action should be equivalent to the user pressing the minimize
+ button in the OS window.
+
+ Note that this command is not available on Fennec. It may also
+ not be available in certain window managers.
+
+ :returns Window rect.
+ """
+ return self._send_message("WebDriver:MinimizeWindow")
+
+ def maximize_window(self):
+ """Resize the browser window currently receiving commands.
+ The action should be equivalent to the user pressing the maximize
+ button in the OS window.
+
+
+ Note that this command is not available on Fennec. It may also
+ not be available in certain window managers.
+
+ :returns: Window rect.
+ """
+ return self._send_message("WebDriver:MaximizeWindow")
+
+ def fullscreen(self):
+ """Synchronously sets the user agent window to full screen as
+ if the user had done "View > Enter Full Screen", or restores
+ it if it is already in full screen.
+
+ :returns: Window rect.
+ """
+ return self._send_message("WebDriver:FullscreenWindow")
diff --git a/testing/marionette/client/marionette_driver/timeout.py b/testing/marionette/client/marionette_driver/timeout.py
new file mode 100644
index 0000000000..27848d0121
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/timeout.py
@@ -0,0 +1,103 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 . import errors
+
+DEFAULT_SCRIPT_TIMEOUT = 30
+DEFAULT_PAGE_LOAD_TIMEOUT = 300
+DEFAULT_IMPLICIT_WAIT_TIMEOUT = 0
+
+
+class Timeouts(object):
+ """Manage timeout settings in the Marionette session.
+
+ Usage::
+
+ marionette = Marionette(...)
+ marionette.start_session()
+ marionette.timeout.page_load = 10
+ marionette.timeout.page_load
+ # => 10
+
+ """
+
+ def __init__(self, marionette):
+ self._marionette = marionette
+
+ def _set(self, name, sec):
+ ms = sec * 1000
+ self._marionette._send_message("WebDriver:SetTimeouts", {name: ms})
+
+ def _get(self, name):
+ ts = self._marionette._send_message("WebDriver:GetTimeouts")
+ if name not in ts:
+ raise KeyError()
+ ms = ts[name]
+ return ms / 1000.0
+
+ @property
+ def script(self):
+ """Get the session's script timeout. This specifies the time
+ to wait for injected scripts to finished before interrupting
+ them. It is by default 30 seconds.
+
+ """
+ return self._get("script")
+
+ @script.setter
+ def script(self, sec):
+ """Set the session's script timeout. This specifies the time
+ to wait for injected scripts to finish before interrupting them.
+
+ """
+ self._set("script", sec)
+
+ @property
+ def page_load(self):
+ """Get the session's page load timeout. This specifies the time
+ to wait for the page loading to complete. It is by default 5
+ minutes (or 300 seconds).
+
+ """
+ # remove fallback when Firefox 56 is stable
+ try:
+ return self._get("pageLoad")
+ except KeyError:
+ return self._get("page load")
+
+ @page_load.setter
+ def page_load(self, sec):
+ """Set the session's page load timeout. This specifies the time
+ to wait for the page loading to complete.
+
+ """
+ # remove fallback when Firefox 56 is stable
+ try:
+ self._set("pageLoad", sec)
+ except errors.InvalidArgumentException:
+ return self._set("page load", sec)
+
+ @property
+ def implicit(self):
+ """Get the session's implicit wait timeout. This specifies the
+ time to wait for the implicit element location strategy when
+ retrieving elements. It is by default disabled (0 seconds).
+
+ """
+ return self._get("implicit")
+
+ @implicit.setter
+ def implicit(self, sec):
+ """Set the session's implicit wait timeout. This specifies the
+ time to wait for the implicit element location strategy when
+ retrieving elements.
+
+ """
+ self._set("implicit", sec)
+
+ def reset(self):
+ """Resets timeouts to their default values."""
+ self.script = DEFAULT_SCRIPT_TIMEOUT
+ self.page_load = DEFAULT_PAGE_LOAD_TIMEOUT
+ self.implicit = DEFAULT_IMPLICIT_WAIT_TIMEOUT
diff --git a/testing/marionette/client/marionette_driver/transport.py b/testing/marionette/client/marionette_driver/transport.py
new file mode 100644
index 0000000000..cbaac8ea2c
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/transport.py
@@ -0,0 +1,409 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.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 socket
+import sys
+import time
+from threading import RLock
+
+import six
+
+
+class SocketTimeout(object):
+ def __init__(self, socket_ctx, timeout):
+ self.socket_ctx = socket_ctx
+ self.timeout = timeout
+ self.old_timeout = None
+
+ def __enter__(self):
+ self.old_timeout = self.socket_ctx.socket_timeout
+ self.socket_ctx.socket_timeout = self.timeout
+
+ def __exit__(self, *args, **kwargs):
+ self.socket_ctx.socket_timeout = self.old_timeout
+
+
+class Message(object):
+ def __init__(self, msgid):
+ self.id = msgid
+
+ def __eq__(self, other):
+ return self.id == other.id
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ # pylint --py3k: W1641
+ return hash(self.id)
+
+
+class Command(Message):
+ TYPE = 0
+
+ def __init__(self, msgid, name, params):
+ Message.__init__(self, msgid)
+ self.name = name
+ self.params = params
+
+ def __str__(self):
+ return "<Command id={0}, name={1}, params={2}>".format(
+ self.id, self.name, self.params
+ )
+
+ def to_msg(self):
+ msg = [Command.TYPE, self.id, self.name, self.params]
+ return json.dumps(msg)
+
+ @staticmethod
+ def from_msg(data):
+ assert data[0] == Command.TYPE
+ cmd = Command(data[1], data[2], data[3])
+ return cmd
+
+
+class Response(Message):
+ TYPE = 1
+
+ def __init__(self, msgid, error, result):
+ Message.__init__(self, msgid)
+ self.error = error
+ self.result = result
+
+ def __str__(self):
+ return "<Response id={0}, error={1}, result={2}>".format(
+ self.id, self.error, self.result
+ )
+
+ def to_msg(self):
+ msg = [Response.TYPE, self.id, self.error, self.result]
+ return json.dumps(msg)
+
+ @staticmethod
+ def from_msg(data):
+ assert data[0] == Response.TYPE
+ return Response(data[1], data[2], data[3])
+
+
+class SocketContext(object):
+ """Object that guards access to a socket via a lock.
+
+ The socket must be accessed using this object as a context manager;
+ access to the socket outside of a context will bypass the lock."""
+
+ def __init__(self, host, port, timeout):
+ self.lock = RLock()
+
+ self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._sock.settimeout(timeout)
+ self._sock.connect((host, port))
+
+ @property
+ def socket_timeout(self):
+ return self._sock.gettimeout()
+
+ @socket_timeout.setter
+ def socket_timeout(self, value):
+ self._sock.settimeout(value)
+
+ def __enter__(self):
+ self.lock.acquire()
+ return self._sock
+
+ def __exit__(self, *args, **kwargs):
+ self.lock.release()
+
+
+class TcpTransport(object):
+ """Socket client that communciates with Marionette via TCP.
+
+ It speaks the protocol of the remote debugger in Gecko, in which
+ messages are always preceded by the message length and a colon, e.g.:
+
+ 7:MESSAGE
+
+ On top of this protocol it uses a Marionette message format, that
+ depending on the protocol level offered by the remote server, varies.
+ Supported protocol levels are `min_protocol_level` and above.
+ """
+
+ max_packet_length = 4096
+ min_protocol_level = 3
+
+ def __init__(self, host, port, socket_timeout=60.0):
+ """If `socket_timeout` is `0` or `0.0`, non-blocking socket mode
+ will be used. Setting it to `1` or `None` disables timeouts on
+ socket operations altogether.
+ """
+ self._socket_context = None
+
+ self.host = host
+ self.port = port
+ self._socket_timeout = socket_timeout
+
+ self.protocol = self.min_protocol_level
+ self.application_type = None
+ self.last_id = 0
+ self.expected_response = None
+
+ @property
+ def socket_timeout(self):
+ return self._socket_timeout
+
+ @socket_timeout.setter
+ def socket_timeout(self, value):
+ self._socket_timeout = value
+
+ if self._socket_context is not None:
+ self._socket_context.socket_timeout = value
+
+ def _unmarshal(self, packet):
+ """Convert data from bytes to a Message subtype
+
+ Message format is [type, msg_id, body1, body2], where body1 and body2 depend
+ on the message type.
+
+ :param packet: Bytes received over the wire representing a complete message.
+ """
+ msg = None
+
+ data = json.loads(packet)
+ msg_type = data[0]
+
+ if msg_type == Command.TYPE:
+ msg = Command.from_msg(data)
+ elif msg_type == Response.TYPE:
+ msg = Response.from_msg(data)
+ else:
+ raise ValueError("Invalid message body {!r}".format(packet))
+
+ return msg
+
+ def receive(self, unmarshal=True):
+ """Wait for the next complete response from the remote.
+
+ Packet format is length-prefixed JSON:
+
+ packet = digit+ ":" body
+ digit = "0"-"9"
+ body = JSON text
+
+ :param unmarshal: Default is to deserialise the packet and
+ return a ``Message`` type. Setting this to false will return
+ the raw packet.
+ """
+ # Initally we read 4 bytes. We don't support reading beyond the end of a message, and
+ # so assuming the JSON body has to be an array or object, the minimum possible message
+ # is 4 bytes: "2:{}". In practice the marionette format has some required fields so the
+ # message is longer, but 4 bytes allows reading messages with bodies up to 999 bytes in
+ # length in two reads, which is the common case.
+ with self._socket_context as sock:
+ recv_bytes = 4
+
+ length_prefix = b""
+
+ body_length = -1
+ body_received = 0
+ body_parts = []
+
+ now = time.time()
+ timeout_time = (
+ now + self.socket_timeout if self.socket_timeout is not None else None
+ )
+
+ while recv_bytes > 0:
+ if timeout_time is not None and time.time() > timeout_time:
+ raise socket.timeout(
+ "Connection timed out after {}s".format(self.socket_timeout)
+ )
+
+ try:
+ chunk = sock.recv(recv_bytes)
+ except socket.timeout:
+ # Lets handle it with our own timeout check
+ continue
+
+ if not chunk:
+ raise socket.error("No data received over socket")
+
+ body_part = None
+ if body_length > 0:
+ body_part = chunk
+ else:
+ parts = chunk.split(b":", 1)
+ length_prefix += parts[0]
+
+ # With > 10 decimal digits we aren't going to have a 32 bit number
+ if len(length_prefix) > 10:
+ raise ValueError(
+ "Invalid message length: {!r}".format(length_prefix)
+ )
+
+ if len(parts) == 2:
+ # We found a : so we know the full length
+ err = None
+ try:
+ body_length = int(length_prefix)
+ except ValueError:
+ err = "expected an integer"
+ else:
+ if body_length <= 0:
+ err = "expected a positive integer"
+ elif body_length > 2**32 - 1:
+ err = "expected a 32 bit integer"
+ if err is not None:
+ raise ValueError(
+ "Invalid message length: {} got {!r}".format(
+ err, length_prefix
+ )
+ )
+ body_part = parts[1]
+
+ # If we didn't find a : yet we keep reading 4 bytes at a time until we do.
+ # We could increase this here to 7 bytes (since we can't have more than 10
+ # length bytes and a seperator byte), or just increase it to
+ # int(length_prefix) + 1 since that's the minimum total number of remaining
+ # bytes (if the : is in the next byte), but it's probably not worth optimising
+ # for large messages.
+
+ if body_part is not None:
+ body_received += len(body_part)
+ body_parts.append(body_part)
+ recv_bytes = body_length - body_received
+
+ body = b"".join(body_parts)
+ if unmarshal:
+ msg = self._unmarshal(body)
+ self.last_id = msg.id
+
+ # keep reading incoming responses until
+ # we receive the user's expected response
+ if isinstance(msg, Response) and msg != self.expected_response:
+ return self.receive(unmarshal)
+
+ return msg
+ return body
+
+ def connect(self):
+ """Connect to the server and process the hello message we expect
+ to receive in response.
+
+ Returns a tuple of the protocol level and the application type.
+ """
+ try:
+ self._socket_context = SocketContext(
+ self.host, self.port, self._socket_timeout
+ )
+ except Exception:
+ # Unset so that the next attempt to send will cause
+ # another connection attempt.
+ self._socket_context = None
+ raise
+
+ try:
+ with SocketTimeout(self._socket_context, 60.0):
+ # first packet is always a JSON Object
+ # which we can use to tell which protocol level we are at
+ raw = self.receive(unmarshal=False)
+ except socket.timeout:
+ exc_cls, exc, tb = sys.exc_info()
+ msg = "Connection attempt failed because no data has been received over the socket: {}"
+ six.reraise(exc_cls, exc_cls(msg.format(exc)), tb)
+
+ hello = json.loads(raw)
+ application_type = hello.get("applicationType")
+ protocol = hello.get("marionetteProtocol")
+
+ if application_type != "gecko":
+ raise ValueError(
+ "Application type '{}' is not supported".format(application_type)
+ )
+
+ if not isinstance(protocol, int) or protocol < self.min_protocol_level:
+ msg = "Earliest supported protocol level is '{}' but got '{}'"
+ raise ValueError(msg.format(self.min_protocol_level, protocol))
+
+ self.application_type = application_type
+ self.protocol = protocol
+
+ return (self.protocol, self.application_type)
+
+ def send(self, obj):
+ """Send message to the remote server. Allowed input is a
+ ``Message`` instance or a JSON serialisable object.
+ """
+ if not self._socket_context:
+ self.connect()
+
+ if isinstance(obj, Message):
+ data = obj.to_msg()
+ if isinstance(obj, Command):
+ self.expected_response = obj
+ else:
+ data = json.dumps(obj)
+ data = six.ensure_binary(data)
+ payload = six.ensure_binary(str(len(data))) + b":" + data
+
+ with self._socket_context as sock:
+ totalsent = 0
+ while totalsent < len(payload):
+ sent = sock.send(payload[totalsent:])
+ if sent == 0:
+ raise IOError(
+ "Socket error after sending {0} of {1} bytes".format(
+ totalsent, len(payload)
+ )
+ )
+ else:
+ totalsent += sent
+
+ def respond(self, obj):
+ """Send a response to a command. This can be an arbitrary JSON
+ serialisable object or an ``Exception``.
+ """
+ res, err = None, None
+ if isinstance(obj, Exception):
+ err = obj
+ else:
+ res = obj
+ msg = Response(self.last_id, err, res)
+ self.send(msg)
+ return self.receive()
+
+ def request(self, name, params):
+ """Sends a message to the remote server and waits for a response
+ to come back.
+ """
+ self.last_id = self.last_id + 1
+ cmd = Command(self.last_id, name, params)
+ self.send(cmd)
+ return self.receive()
+
+ def close(self):
+ """Close the socket.
+
+ First forces the socket to not send data anymore, and then explicitly
+ close it to free up its resources.
+
+ See: https://docs.python.org/2/howto/sockets.html#disconnecting
+ """
+ if self._socket_context:
+ with self._socket_context as sock:
+ try:
+ sock.shutdown(socket.SHUT_RDWR)
+ except IOError as exc:
+ # If the socket is already closed, don't care about:
+ # Errno 57: Socket not connected
+ # Errno 107: Transport endpoint is not connected
+ if exc.errno not in (57, 107):
+ raise
+
+ if sock:
+ # Guard against unclean shutdown.
+ sock.close()
+ self._socket_context = None
+
+ def __del__(self):
+ self.close()
diff --git a/testing/marionette/client/marionette_driver/wait.py b/testing/marionette/client/marionette_driver/wait.py
new file mode 100644
index 0000000000..bc34ccf4cb
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/wait.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 collections
+import sys
+import time
+
+from . import errors
+
+DEFAULT_TIMEOUT = 5
+DEFAULT_INTERVAL = 0.1
+
+
+class Wait(object):
+
+ """An explicit conditional utility class for waiting until a condition
+ evaluates to true or not null.
+
+ This will repeatedly evaluate a condition in anticipation for a
+ truthy return value, or its timeout to expire, or its waiting
+ predicate to become true.
+
+ A `Wait` instance defines the maximum amount of time to wait for a
+ condition, as well as the frequency with which to check the
+ condition. Furthermore, the user may configure the wait to ignore
+ specific types of exceptions whilst waiting, such as
+ `errors.NoSuchElementException` when searching for an element on
+ the page.
+
+ """
+
+ def __init__(
+ self,
+ marionette,
+ timeout=None,
+ interval=None,
+ ignored_exceptions=None,
+ clock=None,
+ ):
+ """Configure the Wait instance to have a custom timeout, interval, and
+ list of ignored exceptions. Optionally a different time
+ implementation than the one provided by the standard library
+ (time) can also be provided.
+
+ Sample usage::
+
+ # Wait 30 seconds for window to open, checking for its presence once
+ # every 5 seconds.
+ wait = Wait(marionette, timeout=30, interval=5,
+ ignored_exceptions=errors.NoSuchWindowException)
+ window = wait.until(lambda m: m.switch_to_window(42))
+
+ :param marionette: The input value to be provided to
+ conditions, usually a Marionette instance.
+
+ :param timeout: How long to wait for the evaluated condition
+ to become true. The default timeout is
+ `wait.DEFAULT_TIMEOUT`.
+
+ :param interval: How often the condition should be evaluated.
+ In reality the interval may be greater as the cost of
+ evaluating the condition function. If that is not the case the
+ interval for the next condition function call is shortend to keep
+ the original interval sequence as best as possible.
+ The default polling interval is `wait.DEFAULT_INTERVAL`.
+
+ :param ignored_exceptions: Ignore specific types of exceptions
+ whilst waiting for the condition. Any exceptions not
+ whitelisted will be allowed to propagate, terminating the
+ wait.
+
+ :param clock: Allows overriding the use of the runtime's
+ default time library. See `wait.SystemClock` for
+ implementation details.
+
+ """
+
+ self.marionette = marionette
+ self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
+ self.interval = interval if interval is not None else DEFAULT_INTERVAL
+ self.clock = clock or SystemClock()
+ self.end = self.clock.now + self.timeout
+
+ exceptions = []
+ if ignored_exceptions is not None:
+ if isinstance(ignored_exceptions, collections.abc.Iterable):
+ exceptions.extend(iter(ignored_exceptions))
+ else:
+ exceptions.append(ignored_exceptions)
+ self.exceptions = tuple(set(exceptions))
+
+ def until(self, condition, is_true=None, message=""):
+ """Repeatedly runs condition until its return value evaluates to true,
+ or its timeout expires or the predicate evaluates to true.
+
+ This will poll at the given interval until the given timeout
+ is reached, or the predicate or conditions returns true. A
+ condition that returns null or does not evaluate to true will
+ fully elapse its timeout before raising an
+ `errors.TimeoutException`.
+
+ If an exception is raised in the condition function and it's
+ not ignored, this function will raise immediately. If the
+ exception is ignored, it will continue polling for the
+ condition until it returns successfully or a
+ `TimeoutException` is raised.
+
+ :param condition: A callable function whose return value will
+ be returned by this function if it evaluates to true.
+
+ :param is_true: An optional predicate that will terminate and
+ return when it evaluates to False. It should be a
+ function that will be passed clock and an end time. The
+ default predicate will terminate a wait when the clock
+ elapses the timeout.
+
+ :param message: An optional message to include in the
+ exception's message if this function times out.
+
+ """
+
+ rv = None
+ last_exc = None
+ until = is_true or until_pred
+ start = self.clock.now
+
+ while not until(self.clock, self.end):
+ try:
+ next = self.clock.now + self.interval
+ rv = condition(self.marionette)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except self.exceptions:
+ last_exc = sys.exc_info()
+
+ # Re-adjust the interval depending on how long the callback
+ # took to evaluate the condition
+ interval_new = max(next - self.clock.now, 0)
+
+ if not rv:
+ self.clock.sleep(interval_new)
+ continue
+
+ if rv is not None:
+ return rv
+
+ self.clock.sleep(interval_new)
+
+ if message:
+ message = " with message: {}".format(message)
+
+ raise errors.TimeoutException(
+ # pylint: disable=W1633
+ "Timed out after {0:.1f} seconds{1}".format(
+ float(round((self.clock.now - start), 1)), message if message else ""
+ ),
+ cause=last_exc,
+ )
+
+
+def until_pred(clock, end):
+ return clock.now >= end
+
+
+class SystemClock(object):
+ def __init__(self):
+ self._time = time
+
+ def sleep(self, duration):
+ self._time.sleep(duration)
+
+ @property
+ def now(self):
+ return self._time.time()
diff --git a/testing/marionette/client/marionette_driver/webauthn.py b/testing/marionette/client/marionette_driver/webauthn.py
new file mode 100644
index 0000000000..4970acbe5a
--- /dev/null
+++ b/testing/marionette/client/marionette_driver/webauthn.py
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+__all__ = ["WebAuthn"]
+
+
+class WebAuthn(object):
+ def __init__(self, marionette):
+ self.marionette = marionette
+
+ def add_virtual_authenticator(self, config):
+ body = {
+ "protocol": config["protocol"],
+ "transport": config["transport"],
+ "hasResidentKey": config.get("hasResidentKey", False),
+ "hasUserVerification": config.get("hasUserVerification", False),
+ "isUserConsenting": config.get("isUserConsenting", True),
+ "isUserVerified": config.get("isUserVerified", False),
+ }
+ return self.marionette._send_message(
+ "WebAuthn:AddVirtualAuthenticator", body, key="value"
+ )
+
+ def remove_virtual_authenticator(self, authenticator_id):
+ body = {"authenticatorId": authenticator_id}
+ return self.marionette._send_message(
+ "WebAuthn:RemoveVirtualAuthenticator", body
+ )
+
+ def add_credential(self, authenticator_id, credential):
+ body = {
+ "authenticatorId": authenticator_id,
+ "credentialId": credential["credentialId"],
+ "isResidentCredential": credential["isResidentCredential"],
+ "rpId": credential["rpId"],
+ "privateKey": credential["privateKey"],
+ "userHandle": credential.get("userHandle"),
+ "signCount": credential.get("signCount", 0),
+ }
+ return self.marionette._send_message("WebAuthn:AddCredential", body)
+
+ def get_credentials(self, authenticator_id):
+ body = {"authenticatorId": authenticator_id}
+ return self.marionette._send_message(
+ "WebAuthn:GetCredentials", body, key="value"
+ )
+
+ def remove_credential(self, authenticator_id, credential_id):
+ body = {"authenticatorId": authenticator_id, "credentialId": credential_id}
+ return self.marionette._send_message("WebAuthn:RemoveCredential", body)
+
+ def remove_all_credentials(self, authenticator_id):
+ body = {"authenticatorId": authenticator_id}
+ return self.marionette._send_message("WebAuthn:RemoveAllCredentials", body)
+
+ def set_user_verified(self, authenticator_id, uv):
+ body = {
+ "authenticatorId": authenticator_id,
+ "isUserVerified": uv["isUserVerified"],
+ }
+ return self.marionette._send_message("WebAuthn:SetUserVerified", body)
diff --git a/testing/marionette/client/requirements.txt b/testing/marionette/client/requirements.txt
new file mode 100644
index 0000000000..220531c4f5
--- /dev/null
+++ b/testing/marionette/client/requirements.txt
@@ -0,0 +1,3 @@
+mozrunner >= 7.4.0
+mozversion >= 2.1.0
+six
diff --git a/testing/marionette/client/setup.py b/testing/marionette/client/setup.py
new file mode 100644
index 0000000000..676266c704
--- /dev/null
+++ b/testing/marionette/client/setup.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 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_driver", "__init__.py"), re.M
+ )[0]
+
+
+setup(
+ name="marionette_driver",
+ version=get_version(),
+ description="Marionette Driver",
+ long_description="""Note marionette_driver is no longer supported.
+
+For more information see https://firefox-source-docs.mozilla.org/python/marionette_driver.html""",
+ # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ classifiers=[
+ "Development Status :: 7 - Inactive",
+ "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="MPL",
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=read("requirements.txt").splitlines(),
+)
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
+ """,
+)
diff --git a/testing/marionette/mach_commands.py b/testing/marionette/mach_commands.py
new file mode 100644
index 0000000000..7736806d1e
--- /dev/null
+++ b/testing/marionette/mach_commands.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 argparse
+import functools
+import logging
+import os
+import sys
+
+from six import iteritems
+
+from mach.decorators import (
+ Command,
+)
+
+from mozbuild.base import (
+ MachCommandConditions as conditions,
+ BinaryNotFoundException,
+)
+
+SUPPORTED_APPS = ["firefox", "android", "thunderbird"]
+
+
+def create_parser_tests():
+ from marionette_harness.runtests import MarionetteArguments
+ from mozlog.structured import commandline
+
+ parser = MarionetteArguments()
+ commandline.add_logging_group(parser)
+ return parser
+
+
+def run_marionette(tests, binary=None, topsrcdir=None, **kwargs):
+ from mozlog.structured import commandline
+
+ from marionette_harness.runtests import MarionetteTestRunner, MarionetteHarness
+
+ parser = create_parser_tests()
+
+ args = argparse.Namespace(tests=tests)
+
+ args.binary = binary
+ args.logger = kwargs.pop("log", None)
+
+ for k, v in iteritems(kwargs):
+ setattr(args, k, v)
+
+ parser.verify_usage(args)
+
+ # Causes Firefox to crash when using non-local connections.
+ os.environ["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
+
+ if not args.logger:
+ args.logger = commandline.setup_logging(
+ "Marionette Unit Tests", args, {"mach": sys.stdout}
+ )
+ failed = MarionetteHarness(MarionetteTestRunner, args=vars(args)).run()
+ if failed > 0:
+ return 1
+ else:
+ return 0
+
+
+@Command(
+ "marionette-test",
+ category="testing",
+ description="Remote control protocol to Gecko, used for browser automation.",
+ conditions=[functools.partial(conditions.is_buildapp_in, apps=SUPPORTED_APPS)],
+ parser=create_parser_tests,
+)
+def marionette_test(command_context, tests, **kwargs):
+ if "test_objects" in kwargs:
+ tests = []
+ for obj in kwargs["test_objects"]:
+ tests.append(obj["file_relpath"])
+ del kwargs["test_objects"]
+
+ if not tests:
+ if conditions.is_thunderbird(command_context):
+ tests = [
+ os.path.join(
+ command_context.topsrcdir,
+ "comm/testing/marionette/unit-tests.ini",
+ )
+ ]
+ else:
+ tests = [
+ os.path.join(
+ command_context.topsrcdir,
+ "testing/marionette/harness/marionette_harness/tests/unit-tests.toml",
+ )
+ ]
+
+ if not kwargs.get("binary") and (
+ conditions.is_firefox(command_context)
+ or conditions.is_thunderbird(command_context)
+ ):
+ try:
+ kwargs["binary"] = command_context.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ command_context.log(
+ logging.ERROR,
+ "marionette-test",
+ {"error": str(e)},
+ "ERROR: {error}",
+ )
+ command_context.log(
+ logging.INFO, "marionette-test", {"help": e.help()}, "{help}"
+ )
+ return 1
+
+ return run_marionette(tests, topsrcdir=command_context.topsrcdir, **kwargs)
diff --git a/testing/marionette/mach_test_package_commands.py b/testing/marionette/mach_test_package_commands.py
new file mode 100644
index 0000000000..0bb3df8b89
--- /dev/null
+++ b/testing/marionette/mach_test_package_commands.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/.
+
+import argparse
+import os
+import sys
+from functools import partial
+
+from mach.decorators import Command
+
+parser = None
+
+
+def run_marionette(context, **kwargs):
+ from marionette.runtests import MarionetteHarness, MarionetteTestRunner
+ from mozlog.structured import commandline
+
+ args = argparse.Namespace(**kwargs)
+ args.binary = args.binary or context.firefox_bin
+
+ test_root = os.path.join(context.package_root, "marionette", "tests")
+ if not args.tests:
+ args.tests = [
+ os.path.join(
+ test_root,
+ "testing",
+ "marionette",
+ "harness",
+ "marionette_harness",
+ "tests",
+ "unit-tests.toml",
+ )
+ ]
+
+ normalize = partial(context.normalize_test_path, test_root)
+ args.tests = list(map(normalize, args.tests))
+
+ commandline.add_logging_group(parser)
+ parser.verify_usage(args)
+
+ args.logger = commandline.setup_logging(
+ "Marionette Unit Tests", args, {"mach": sys.stdout}
+ )
+ status = MarionetteHarness(MarionetteTestRunner, args=vars(args)).run()
+ return 1 if status else 0
+
+
+def setup_marionette_argument_parser():
+ from marionette.runner.base import BaseMarionetteArguments
+
+ global parser
+ parser = BaseMarionetteArguments()
+ return parser
+
+
+@Command(
+ "marionette-test",
+ category="testing",
+ description="Run a Marionette test (Check UI or the internal JavaScript "
+ "using marionette).",
+ parser=setup_marionette_argument_parser,
+)
+def run_marionette_test(command_context, **kwargs):
+ command_context.context.activate_mozharness_venv()
+ return run_marionette(command_context.context, **kwargs)
diff --git a/testing/marionette/moz.build b/testing/marionette/moz.build
new file mode 100644
index 0000000000..1986d20904
--- /dev/null
+++ b/testing/marionette/moz.build
@@ -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/.
+
+MARIONETTE_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.toml"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Testing", "Marionette Client and Harness")
+
+with Files("harness/**"):
+ SCHEDULES.exclusive = ["marionette", "firefox-ui"]
+
+SPHINX_PYTHON_PACKAGE_DIRS += ["client/marionette_driver"]