From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../harness/marionette_harness/__init__.py | 32 + .../marionette_harness/certificates/test.cert | 86 ++ .../marionette_harness/certificates/test.key | 28 + .../marionette_harness/marionette_test/__init__.py | 24 + .../marionette_test/decorators.py | 194 +++ .../marionette_test/testcases.py | 420 +++++++ .../harness/marionette_harness/runner/__init__.py | 16 + .../harness/marionette_harness/runner/base.py | 1265 ++++++++++++++++++++ .../harness/marionette_harness/runner/httpd.py | 243 ++++ .../marionette_harness/runner/mixins/__init__.py | 5 + .../runner/mixins/window_manager.py | 210 ++++ .../harness/marionette_harness/runner/serve.py | 239 ++++ .../harness/marionette_harness/runtests.py | 115 ++ .../tests/harness_unit/conftest.py | 99 ++ .../tests/harness_unit/python.toml | 14 + .../tests/harness_unit/test_httpd.py | 92 ++ .../harness_unit/test_marionette_arguments.py | 80 ++ .../tests/harness_unit/test_marionette_harness.py | 110 ++ .../tests/harness_unit/test_marionette_runner.py | 541 +++++++++ .../harness_unit/test_marionette_test_result.py | 55 + .../tests/harness_unit/test_serve.py | 69 ++ .../marionette_harness/tests/unit-tests.toml | 43 + .../marionette_harness/tests/unit/data/test.html | 13 + .../tests/unit/test_accessibility.py | 241 ++++ .../tests/unit/test_actions_key.py | 71 ++ .../tests/unit/test_actions_pointer.py | 134 +++ .../tests/unit/test_actions_wheel.py | 68 ++ .../marionette_harness/tests/unit/test_addons.py | 140 +++ .../tests/unit/test_capabilities.py | 322 +++++ .../marionette_harness/tests/unit/test_checkbox.py | 17 + .../tests/unit/test_checkbox_chrome.py | 33 + .../marionette_harness/tests/unit/test_chrome.py | 31 + .../tests/unit/test_chrome_action.py | 61 + .../tests/unit/test_chrome_element_css.py | 31 + .../tests/unit/test_cli_arguments.py | 98 ++ .../marionette_harness/tests/unit/test_click.py | 571 +++++++++ .../tests/unit/test_click_chrome.py | 33 + .../tests/unit/test_click_scrolling.py | 167 +++ .../marionette_harness/tests/unit/test_context.py | 82 ++ .../marionette_harness/tests/unit/test_cookies.py | 115 ++ .../marionette_harness/tests/unit/test_crash.py | 211 ++++ .../tests/unit/test_data_driven.py | 72 ++ .../tests/unit/test_date_time_value.py | 33 + .../tests/unit/test_element_id.py | 55 + .../tests/unit/test_element_id_chrome.py | 88 ++ .../tests/unit/test_element_rect.py | 22 + .../tests/unit/test_element_rect_chrome.py | 30 + .../tests/unit/test_element_state.py | 175 +++ .../tests/unit/test_element_state_chrome.py | 56 + .../marionette_harness/tests/unit/test_errors.py | 105 ++ .../tests/unit/test_execute_async_script.py | 240 ++++ .../tests/unit/test_execute_isolate.py | 46 + .../tests/unit/test_execute_sandboxes.py | 86 ++ .../tests/unit/test_execute_script.py | 569 +++++++++ .../marionette_harness/tests/unit/test_expected.py | 233 ++++ .../tests/unit/test_expectedfail.py | 11 + .../tests/unit/test_file_upload.py | 169 +++ .../tests/unit/test_findelement.py | 479 ++++++++ .../tests/unit/test_findelement_chrome.py | 169 +++ .../tests/unit/test_geckoinstance.py | 25 + .../tests/unit/test_get_computed_label.py | 26 + .../tests/unit/test_get_computed_role.py | 26 + .../tests/unit/test_get_current_url_chrome.py | 39 + .../tests/unit/test_get_shadow_root.py | 66 + .../tests/unit/test_implicit_waits.py | 26 + .../tests/unit/test_localization.py | 71 ++ .../tests/unit/test_marionette.py | 138 +++ .../tests/unit/test_modal_dialogs.py | 161 +++ .../tests/unit/test_navigation.py | 901 ++++++++++++++ .../tests/unit/test_pagesource.py | 52 + .../tests/unit/test_pagesource_chrome.py | 26 + .../marionette_harness/tests/unit/test_position.py | 46 + .../marionette_harness/tests/unit/test_prefs.py | 213 ++++ .../tests/unit/test_prefs_enforce.py | 54 + .../tests/unit/test_profile_management.py | 267 +++++ .../marionette_harness/tests/unit/test_proxy.py | 159 +++ .../tests/unit/test_quit_restart.py | 550 +++++++++ .../marionette_harness/tests/unit/test_reftest.py | 105 ++ .../tests/unit/test_rendered_element.py | 31 + .../marionette_harness/tests/unit/test_report.py | 27 + .../tests/unit/test_run_js_test.py | 10 + .../tests/unit/test_screen_orientation.py | 75 ++ .../tests/unit/test_screenshot.py | 393 ++++++ .../marionette_harness/tests/unit/test_select.py | 218 ++++ .../tests/unit/test_sendkeys_menupopup_chrome.py | 106 ++ .../marionette_harness/tests/unit/test_session.py | 49 + .../tests/unit/test_shadowroot_findelement.py | 113 ++ .../tests/unit/test_skip_setup.py | 33 + .../tests/unit/test_switch_frame.py | 96 ++ .../tests/unit/test_switch_frame_chrome.py | 57 + .../tests/unit/test_switch_window_chrome.py | 113 ++ .../tests/unit/test_switch_window_content.py | 258 ++++ .../tests/unit/test_teardown_context_preserved.py | 21 + .../marionette_harness/tests/unit/test_text.py | 26 + .../tests/unit/test_text_chrome.py | 35 + .../marionette_harness/tests/unit/test_timeouts.py | 113 ++ .../marionette_harness/tests/unit/test_title.py | 17 + .../tests/unit/test_title_chrome.py | 37 + .../tests/unit/test_transport.py | 110 ++ .../marionette_harness/tests/unit/test_typing.py | 374 ++++++ .../tests/unit/test_unhandled_prompt_behavior.py | 126 ++ .../tests/unit/test_visibility.py | 175 +++ .../marionette_harness/tests/unit/test_wait.py | 347 ++++++ .../tests/unit/test_window_close_chrome.py | 73 ++ .../tests/unit/test_window_close_content.py | 109 ++ .../tests/unit/test_window_handles_chrome.py | 253 ++++ .../tests/unit/test_window_handles_content.py | 156 +++ .../tests/unit/test_window_management.py | 141 +++ .../tests/unit/test_window_maximize.py | 36 + .../tests/unit/test_window_rect.py | 315 +++++ .../tests/unit/test_window_status_chrome.py | 23 + .../tests/unit/test_window_status_content.py | 94 ++ .../tests/unit/test_window_type_chrome.py | 26 + .../tests/unit/test_windowless.py | 60 + .../marionette_harness/tests/unit/unit-tests.toml | 193 +++ .../tests/unit/webextension-invalid.xpi | Bin 0 -> 295 bytes .../tests/unit/webextension-signed.xpi | Bin 0 -> 4221 bytes .../tests/unit/webextension-unsigned.xpi | Bin 0 -> 310 bytes .../marionette_harness/www/actions_scroll.html | 139 +++ .../www/addons/webextension-signed.xpi | Bin 0 -> 4221 bytes .../www/addons/webextension-unsigned.xpi | Bin 0 -> 310 bytes .../harness/marionette_harness/www/black.png | Bin 0 -> 150 bytes .../harness/marionette_harness/www/bug814037.html | 56 + .../www/click_out_of_bounds_overflow.html | 90 ++ .../harness/marionette_harness/www/clicks.html | 57 + .../www/dom/cache/basicCacheAPI_PBM.html | 21 + .../www/dom/cache/cacheUsage.html | 28 + .../www/dom/indexedDB/basicIDB_PBM.html | 49 + .../www/element_outside_viewport.html | 41 + .../harness/marionette_harness/www/empty.html | 12 + .../harness/marionette_harness/www/formPage.html | 114 ++ .../harness/marionette_harness/www/frameset.html | 13 + .../marionette_harness/www/framesetPage2.html | 7 + .../harness/marionette_harness/www/html5/blue.jpg | Bin 0 -> 92 bytes .../www/html5/boolean_attributes.html | 2 + .../marionette_harness/www/html5/geolocation.js | 29 + .../harness/marionette_harness/www/html5/green.jpg | Bin 0 -> 92 bytes .../marionette_harness/www/html5/offline.html | 1 + .../harness/marionette_harness/www/html5/red.jpg | Bin 0 -> 92 bytes .../marionette_harness/www/html5/status.html | 1 + .../marionette_harness/www/html5/test.appcache | 11 + .../www/html5/test_html_inputs.html | 2 + .../marionette_harness/www/html5/yellow.jpg | Bin 0 -> 92 bytes .../harness/marionette_harness/www/html5Page.html | 46 + .../harness/marionette_harness/www/keyboard.html | 99 ++ .../www/layout/test_carets_columns.html | 31 + .../www/layout/test_carets_cursor.html | 31 + .../www/layout/test_carets_display_none.html | 10 + .../www/layout/test_carets_iframe.html | 15 + .../www/layout/test_carets_iframe_scroll.html | 11 + .../layout/test_carets_iframe_scroll_inner.html | 24 + .../www/layout/test_carets_key_scroll.html | 18 + .../www/layout/test_carets_longtext.html | 9 + .../www/layout/test_carets_multipleline.html | 18 + .../www/layout/test_carets_multiplerange.html | 19 + .../www/layout/test_carets_selection.html | 49 + .../www/layout/test_carets_svg_shapes.html | 12 + .../www/navigation_pushstate.html | 20 + .../www/navigation_pushstate_target.html | 13 + .../marionette_harness/www/nestedElements.html | 9 + .../www/reftest/mostly-teal-700x700.html | 21 + .../www/reftest/teal-700x700.html | 21 + .../harness/marionette_harness/www/resultPage.html | 16 + .../www/serviceworker/install_serviceworker.html | 11 + .../www/serviceworker/serviceworker.js | 0 .../harness/marionette_harness/www/shim.js | 297 +++++ .../marionette_harness/www/slow_resource.html | 13 + .../harness/marionette_harness/www/test.html | 43 + .../marionette_harness/www/test_accessibility.html | 57 + .../marionette_harness/www/test_clearing.html | 24 + .../marionette_harness/www/test_dynamic.html | 38 + .../marionette_harness/www/test_iframe.html | 16 + .../marionette_harness/www/test_inner_iframe.html | 13 + .../marionette_harness/www/test_nested_iframe.html | 13 + .../harness/marionette_harness/www/test_oop_1.html | 14 + .../harness/marionette_harness/www/test_oop_2.html | 14 + .../marionette_harness/www/test_windows.html | 13 + .../marionette_harness/www/update/complete.mar | Bin 0 -> 86612 bytes .../www/update/complete.mar.headers | 1 + .../harness/marionette_harness/www/visibility.html | 51 + .../harness/marionette_harness/www/white.png | Bin 0 -> 150 bytes .../marionette_harness/www/windowHandles.html | 16 + .../harness/marionette_harness/www/xhtmlTest.html | 79 ++ 183 files changed, 18325 insertions(+) create mode 100644 testing/marionette/harness/marionette_harness/__init__.py create mode 100644 testing/marionette/harness/marionette_harness/certificates/test.cert create mode 100644 testing/marionette/harness/marionette_harness/certificates/test.key create mode 100644 testing/marionette/harness/marionette_harness/marionette_test/__init__.py create mode 100644 testing/marionette/harness/marionette_harness/marionette_test/decorators.py create mode 100644 testing/marionette/harness/marionette_harness/marionette_test/testcases.py create mode 100644 testing/marionette/harness/marionette_harness/runner/__init__.py create mode 100644 testing/marionette/harness/marionette_harness/runner/base.py create mode 100755 testing/marionette/harness/marionette_harness/runner/httpd.py create mode 100644 testing/marionette/harness/marionette_harness/runner/mixins/__init__.py create mode 100644 testing/marionette/harness/marionette_harness/runner/mixins/window_manager.py create mode 100755 testing/marionette/harness/marionette_harness/runner/serve.py create mode 100644 testing/marionette/harness/marionette_harness/runtests.py create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/python.toml create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/test_httpd.py create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_harness.py create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_test_result.py create mode 100644 testing/marionette/harness/marionette_harness/tests/harness_unit/test_serve.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit-tests.toml create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/data/test.html create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_actions_key.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_actions_pointer.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_actions_wheel.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_addons.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_checkbox.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_checkbox_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_chrome_action.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_chrome_element_css.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_cli_arguments.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_click.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_click_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_click_scrolling.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_context.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_crash.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_data_driven.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_date_time_value.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_element_id.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_element_id_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_element_rect.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_element_rect_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_element_state.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_element_state_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_errors.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_execute_async_script.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_execute_isolate.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_execute_sandboxes.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_expected.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_expectedfail.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_file_upload.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_findelement.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_findelement_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_geckoinstance.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_label.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_role.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_get_current_url_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_get_shadow_root.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_implicit_waits.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_localization.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_pagesource.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_pagesource_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_position.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_prefs_enforce.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_profile_management.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_quit_restart.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_reftest.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_rendered_element.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_report.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_run_js_test.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_screen_orientation.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_screenshot.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_select.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_sendkeys_menupopup_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_session.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_shadowroot_findelement.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_skip_setup.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_content.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_teardown_context_preserved.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_text.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_text_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_title.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_title_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_transport.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_typing.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_unhandled_prompt_behavior.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_visibility.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_wait.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_close_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_close_content.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_content.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_management.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_rect.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_status_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_status_content.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_window_type_chrome.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/test_windowless.py create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/unit-tests.toml create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/webextension-invalid.xpi create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpi create mode 100644 testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpi create mode 100644 testing/marionette/harness/marionette_harness/www/actions_scroll.html create mode 100644 testing/marionette/harness/marionette_harness/www/addons/webextension-signed.xpi create mode 100644 testing/marionette/harness/marionette_harness/www/addons/webextension-unsigned.xpi create mode 100644 testing/marionette/harness/marionette_harness/www/black.png create mode 100644 testing/marionette/harness/marionette_harness/www/bug814037.html create mode 100644 testing/marionette/harness/marionette_harness/www/click_out_of_bounds_overflow.html create mode 100644 testing/marionette/harness/marionette_harness/www/clicks.html create mode 100644 testing/marionette/harness/marionette_harness/www/dom/cache/basicCacheAPI_PBM.html create mode 100644 testing/marionette/harness/marionette_harness/www/dom/cache/cacheUsage.html create mode 100644 testing/marionette/harness/marionette_harness/www/dom/indexedDB/basicIDB_PBM.html create mode 100644 testing/marionette/harness/marionette_harness/www/element_outside_viewport.html create mode 100644 testing/marionette/harness/marionette_harness/www/empty.html create mode 100644 testing/marionette/harness/marionette_harness/www/formPage.html create mode 100644 testing/marionette/harness/marionette_harness/www/frameset.html create mode 100644 testing/marionette/harness/marionette_harness/www/framesetPage2.html create mode 100644 testing/marionette/harness/marionette_harness/www/html5/blue.jpg create mode 100644 testing/marionette/harness/marionette_harness/www/html5/boolean_attributes.html create mode 100644 testing/marionette/harness/marionette_harness/www/html5/geolocation.js create mode 100644 testing/marionette/harness/marionette_harness/www/html5/green.jpg create mode 100644 testing/marionette/harness/marionette_harness/www/html5/offline.html create mode 100644 testing/marionette/harness/marionette_harness/www/html5/red.jpg create mode 100644 testing/marionette/harness/marionette_harness/www/html5/status.html create mode 100644 testing/marionette/harness/marionette_harness/www/html5/test.appcache create mode 100644 testing/marionette/harness/marionette_harness/www/html5/test_html_inputs.html create mode 100644 testing/marionette/harness/marionette_harness/www/html5/yellow.jpg create mode 100644 testing/marionette/harness/marionette_harness/www/html5Page.html create mode 100644 testing/marionette/harness/marionette_harness/www/keyboard.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_columns.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_cursor.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_display_none.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_iframe_scroll_inner.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_key_scroll.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_longtext.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_multipleline.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_multiplerange.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_selection.html create mode 100644 testing/marionette/harness/marionette_harness/www/layout/test_carets_svg_shapes.html create mode 100644 testing/marionette/harness/marionette_harness/www/navigation_pushstate.html create mode 100644 testing/marionette/harness/marionette_harness/www/navigation_pushstate_target.html create mode 100644 testing/marionette/harness/marionette_harness/www/nestedElements.html create mode 100644 testing/marionette/harness/marionette_harness/www/reftest/mostly-teal-700x700.html create mode 100644 testing/marionette/harness/marionette_harness/www/reftest/teal-700x700.html create mode 100644 testing/marionette/harness/marionette_harness/www/resultPage.html create mode 100644 testing/marionette/harness/marionette_harness/www/serviceworker/install_serviceworker.html create mode 100644 testing/marionette/harness/marionette_harness/www/serviceworker/serviceworker.js create mode 100644 testing/marionette/harness/marionette_harness/www/shim.js create mode 100644 testing/marionette/harness/marionette_harness/www/slow_resource.html create mode 100644 testing/marionette/harness/marionette_harness/www/test.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_accessibility.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_clearing.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_dynamic.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_iframe.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_inner_iframe.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_nested_iframe.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_oop_1.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_oop_2.html create mode 100644 testing/marionette/harness/marionette_harness/www/test_windows.html create mode 100644 testing/marionette/harness/marionette_harness/www/update/complete.mar create mode 100644 testing/marionette/harness/marionette_harness/www/update/complete.mar.headers create mode 100644 testing/marionette/harness/marionette_harness/www/visibility.html create mode 100644 testing/marionette/harness/marionette_harness/www/white.png create mode 100644 testing/marionette/harness/marionette_harness/www/windowHandles.html create mode 100644 testing/marionette/harness/marionette_harness/www/xhtmlTest.html (limited to 'testing/marionette/harness/marionette_harness') 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 = """ +HTTP Authentication +

{}

""" + + 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 = """ + +Slow page loading + +

Delay: {}

+""".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 = """ + +Slow cross-origin page loading + +

Delay: {}

+""".format( + delay + ) + + +@handlers.handler +def update_xml_handler(request, response): + response.headers.set("Content-Type", "text/xml") + mar_digest = ( + "75cd68e6c98c84c435cd27e353f5b4f6a3f2c50f6802aa9bf62b47e47138757306769fd9befa08793635ee649" + "2319253480860b4aa8ed9ee1caaa4c83ebc90b9" + ) + response.content = """ + + + + + + """.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 ..." + ) + 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: "".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 @@ + + + + + +Marionette Test + + +

Loaded via file://

+ + 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( + """ +
foo
+ """ + ) + ) + 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( + """ + + + """ + ) + ) + + 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 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( + """ + + +
overlay
+
link + + +""" +) + + +obscured_overlay = inline( + """ + + +
+link + + +""" +) + + +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 = 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( + """ + + """ + ) + ) + + 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( + """ + click me + + """ + ) + ) + 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 = self.marionette.find_element(By.TAG_NAME, "select") + + # This tests that the pointer-interactability test does not + # cause an ElementClickInterceptedException. + # + # At a . + 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( + """ + """ + ) + ) + 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( + """ + """ + ) + ) + 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( + """ + """ + ) + ) + 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( + """ + + +
+ foo +
""" + ) + ) + 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( + """ + + +
""" + ) + ) + + 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( + """ + + +
""" + ) + ) + + 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( + """ + + +
""" + ) + ) + + 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( + """ + + +
""" + ) + ) + + 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("")) + 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 = 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 = self.marionette.find_element(By.TAG_NAME, "button") + # should not time out + button.click() + + def test_stop_propagation(self): + self.marionette.navigate( + inline( + """ + + + """ + ) + ) + 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 = 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( + """ + Link to content +
Text
+ """ + ) + ) + + # 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( + """ +
+ + +
+ """ + ) + ) + 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( + """ + + """ + ) + ) + self.marionette.find_element(By.ID, "radio").click() + + def test_overflow_scroll_do_not_scroll_elements_which_are_visible(self): + self.marionette.navigate( + inline( + """ +
    +
  • +
  • Text
  • +
  • +
  • +
+ """ + ) + ) + + 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: +
    +
  • line1
  • +
  • line2
  • +
  • line3
  • +
  • line4
  • +
+ """ + ) + ) + + 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: +
+
+
+ """ + ) + ) + + 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("")) + + 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("")) + + 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("

") + + +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("""link""")) + 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""" + + + XHTML might be the future + + + + {} + +""".format( + doc + ) + ) + ) + + +attribute = inline("") +input = inline("") +disabled = inline("") +check = inline("") + + +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("

")) + 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("

foo")) + el = self.marionette.find_element(By.TAG_NAME, "p") + attr = el.get_attribute("hidden") + self.assertIsNone(attr) + + self.marionette.navigate(inline("

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("

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('

', 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("

foo

bar

") + +shadow_dom = """ + + + """ + + +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( + """ + """ + ) + ) + + # 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( + """ + + """ + ) + ) + self.marionette.execute_script("", sandbox=None) + + # removeEventListener is called when sandbox is unloaded + self.marionette.navigate( + inline( + """ + + """ + ) + ) + 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("""

foo

""") +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("

hidden

") + +selected_element = inline("") +unselected_element = inline("") + +enabled_element = inline("") +disabled_element = inline("") + + +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("")) +multiple = "data:text/html,{}".format(quote("")) +upload = lambda url: "data:text/html,{}".format( + quote( + """ +
+ + +
""".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""" + + + XHTML might be the future + + + + {} + +""".format( + doc + ) + ) + ) + + +id_html = inline("

", doctype="html") +id_xhtml = inline('

', doctype="xhtml") +parent_child_html = inline("

", doctype="html") +parent_child_xhtml = inline( + '

', doctype="xhtml" +) +children_html = inline("

foo

bar

", doctype="html") +children_xhtml = inline("

foo

bar

", doctype="xhtml") +class_html = inline("

", doctype="html") +class_xhtml = inline('

', doctype="xhtml") +name_html = inline("

", doctype="html") +name_xhtml = inline('

', 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("