summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webdriver
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/webdriver
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webdriver')
-rw-r--r--testing/web-platform/tests/webdriver/META.yml10
-rw-r--r--testing/web-platform/tests/webdriver/README.md17
-rw-r--r--testing/web-platform/tests/webdriver/tests/__init__.py4
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/__init__.py168
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/__init__.py35
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/create_user_context.py60
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/get_user_contexts.py45
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/invalid.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/user_context.py75
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/__init__.py97
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/__init__.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/activate.py95
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/invalid.py37
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/__init__.py109
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py81
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py375
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/format.py39
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/frame.py59
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/invalid.py149
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/origin.py56
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/window_handle.py7
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/close.py23
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/invalid.py31
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/context_created.py265
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/context_destroyed.py292
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/background.py32
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/invalid.py90
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py66
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/type.py41
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/user_context.py118
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/dom_content_loaded.py195
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/fragment_navigated.py311
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/history_api.py57
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py183
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/invalid.py27
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/handle_user_prompt.py178
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/invalid.py39
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/load.py175
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/context.py88
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py228
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py207
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py181
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/ownership.py26
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/sandbox.py111
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/serialization_options.py65
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py179
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/__init__.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/about_blank.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/data_url.py101
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/error.py22
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/frame.py59
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/hash.py90
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/image.py56
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/invalid.py53
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/navigate.py88
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/wait.py98
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/navigation_started.py463
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/background.py58
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/context.py61
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/invalid.py200
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/margin.py215
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/orientation.py43
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page.py39
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page_ranges.py131
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/scale.py57
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/shrink_to_fit.py50
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/__init__.py40
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/frame.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/invalid.py37
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/reload.py73
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/wait.py173
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py70
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/invalid.py91
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py186
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/black_dot.pngbin0 -> 70 bytes
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.html0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.js1
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.svg2
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.html0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.svg3
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/red_dot.pngbin0 -> 95 bytes
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/conftest.py38
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/context.py55
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/delta.py167
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/invalid.py44
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/user_prompt_closed.py270
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/user_prompt_opened.py183
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/errors/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/errors/errors.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/external/permissions/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/__init__.py23
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py54
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py105
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/__init__.py42
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/conftest.py45
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/__init__.py137
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/invalid.py904
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key.py107
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py268
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_modifier.py163
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer.py15
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py318
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_drag.py137
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_modifier.py242
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_multiclick.py162
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_origin.py196
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_pen.py126
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_touch.py195
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py161
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel_origin.py55
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/context.py42
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/invalid.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/release.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/sequence.py82
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/__init__.py129
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console.py191
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console_args.py274
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/event_buffer.py95
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/javascript.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/realm.py32
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/stacktrace.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/subscription.py110
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/__init__.py351
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/add_intercept.py170
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/invalid.py187
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phase_auth_required.py145
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phases.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/url_patterns.py220
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/auth_required.py76
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/unsubscribe.py35
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/before_request_sent.py395
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/combined/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/combined/network_events.py269
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/conftest.py206
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/invalid.py318
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/request.py50
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/credentials.py78
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/invalid.py455
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/request.py57
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/action.py148
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/invalid.py181
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/invalid.py45
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/request.py29
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/fetch_error.py297
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/invalid.py407
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/request.py67
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/invalid.py28
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/remove_intercept.py106
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py370
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_cached.py191
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py55
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_started/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py311
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started_cached.py199
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.html2
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.js1
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.pngbin0 -> 72 bytes
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.svg1
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.txt1
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/other.txt1
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/redirect_http_equiv.html4
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/network/support/redirected.html2
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/__init__.py226
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/add_preload_script.py172
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/arguments.py238
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/contexts.py111
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/invalid.py250
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/sandbox.py70
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/arguments.py101
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/await_promise.py46
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/channel.py217
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details.py86
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details_await_promise.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/function_declaration.py14
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/internal_id.py67
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/invalid.py433
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/primitive_values.py22
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/realm.py71
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_reference.py338
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_values.py179
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_node.py759
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_ownership.py60
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/sandbox.py239
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/serialization_options.py569
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/strict_mode.py40
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/target.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/this.py149
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/call_function/user_activation.py42
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/node_shared_id.py101
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/window_reference.py124
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/conftest.py67
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/disown/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/disown/handles.py173
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/disown/invalid.py68
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/disown/target.py136
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/await_promise.py202
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/evaluate.py95
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details.py84
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details_await_promise.py32
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/internal_id.py65
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/invalid.py164
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/primitive_values.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/remote_values.py145
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_node.py741
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_ownership.py60
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/sandbox.py199
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/serialization_options.py569
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/strict_mode.py34
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/target.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/user_activation.py41
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/context.py70
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/get_realms.py183
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/invalid.py26
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/sandbox.py238
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/type.py34
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/message/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/message/message.py101
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/realm_created.py365
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/realm_destroyed.py342
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/invalid.py15
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/remove_preload_script.py120
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/sandbox.py42
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/new/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/new/connect.py38
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/status/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/status/status.py10
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/contexts.py275
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/events.py136
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/invalid.py156
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/contexts.py165
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/events.py81
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/invalid.py234
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py90
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/conftest.py11
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/filter.py559
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/invalid.py155
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/partition.py258
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_domain.py19
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_expiry.py51
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_http_only.py29
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_name.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_path.py25
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_same_site.py26
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_secure.py25
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_value.py20
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/invalid.py126
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/page_protocols.py25
-rw-r--r--testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/partition.py78
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/accept_alert/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/accept_alert/accept.py110
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/add_cookie/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py288
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/add_cookie/user_prompts.py137
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/back/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/back/back.py134
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/back/conftest.py19
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/back/user_prompts.py191
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/close_window/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/close_window/close.py102
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/close_window/user_prompts.py187
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/delete.py22
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/user_prompts.py117
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_cookie/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_cookie/delete.py29
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_cookie/user_prompts.py119
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_session/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/delete_session/delete.py42
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/dismiss.py109
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py452
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py131
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/bubbling.py157
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/center_point.py64
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/click.py99
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/events.py34
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/file_upload.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/interactability.py130
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py189
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/scroll_into_view.py72
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/select.py223
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/shadow_dom.py60
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/support/input.html3
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/support/test_click_wdspec.html100
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_click/user_prompts.py198
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/__init__.py2
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/conftest.py17
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/content_editable.py30
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/events.py85
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/file_upload.py262
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/form_controls.py102
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/interactability.py142
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/scroll_into_view.py40
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/send_keys.py132
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/element_send_keys/user_prompts.py123
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/__init__.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/arguments.py205
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/collections.py165
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/cyclic.py78
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/execute_async.py79
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/node.py86
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/objects.py49
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/promise.py118
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/properties.py64
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/user_prompts.py195
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_async_script/window.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/__init__.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/arguments.py190
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/collections.py143
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/cyclic.py78
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/execute.py114
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/node.py85
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/objects.py49
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/promise.py102
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/properties.py60
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/user_prompts.py189
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/execute_script/window.py87
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element/find.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element/user_prompts.py120
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/find.py179
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/user_prompts.py125
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/find.py247
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/user_prompts.py134
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements/find.py141
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements/user_prompts.py122
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/find.py199
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/user_prompts.py127
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/find.py260
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/user_prompts.py135
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/forward/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/forward/conftest.py19
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/forward/forward.py176
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/forward/user_prompts.py195
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/fullscreen.py88
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/stress.py22
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/user_prompts.py116
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_active_element/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_active_element/get.py154
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_active_element/user_prompts.py118
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_alert_text/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_alert_text/get.py73
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_computed_label/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_computed_label/get.py88
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_computed_role/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_computed_role/get.py86
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_current_url/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_current_url/file.py23
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_current_url/get.py73
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_current_url/iframe.py75
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_current_url/user_prompts.py111
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/get.py167
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/user_prompts.py117
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/get.py107
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/user_prompts.py120
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_property/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_property/get.py215
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_property/user_prompts.py115
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_rect/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_rect/get.py99
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_rect/user_prompts.py120
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/get.py102
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/user_prompts.py117
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/get.py95
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/user_prompts.py114
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_text/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py145
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_element_text/user_prompts.py116
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py144
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/user_prompts.py116
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_page_source/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_page_source/source.py25
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_page_source/user_prompts.py112
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_timeouts/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_timeouts/get.py34
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_title/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_title/get.py56
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_title/iframe.py80
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_title/user_prompts.py134
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_handle/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_handle/get.py38
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_handle/user_prompts.py61
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_handles/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_handles/get.py37
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_handles/user_prompts.py61
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_rect/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_rect/get.py31
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/get_window_rect/user_prompts.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/idlharness.window.js16
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/interface/interface.py2
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py171
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py119
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_selected/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_selected/selected.py138
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/is_element_selected/user_prompts.py117
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/maximize_window/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/maximize_window/maximize.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/maximize_window/stress.py45
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/maximize_window/user_prompts.py117
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/minimize_window/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/minimize_window/minimize.py83
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/minimize_window/stress.py22
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/minimize_window/user_prompts.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/navigate_to/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/navigate_to/file.py25
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/navigate_to/navigate.py93
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/navigate_to/user_prompts.py185
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/conftest.py82
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/create_alwaysMatch.py15
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/create_firstMatch.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/default_values.py40
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/invalid_capabilities.py56
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/merge.py82
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/no_capabilities.py8
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/page_load_strategy.py7
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/platform_name.py11
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/response.py44
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/support/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/support/create.py136
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/timeouts.py32
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_session/websocket_url.py7
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_window/__init__.py10
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_window/new.py64
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_window/new_tab.py89
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_window/new_window.py90
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/new_window/user_prompts.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/__init__.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/conftest.py89
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/invalid.py842
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/key.py62
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_events.py223
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_modifiers.py37
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_shortcuts.py47
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_special_keys.py38
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/none.py17
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/perform.py55
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_contextmenu.py78
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_dblclick.py58
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_modifier_click.py91
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse.py289
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_origin.py123
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pause_dblclick.py56
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pen.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_touch.py145
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_tripleclick.py30
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/sequence.py7
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/mouse.py26
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/refine.py29
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/user_prompts.py144
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/permissions/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/permissions/set.py119
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/print/__init__.py21
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/print/background.py58
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/print/orientation.py43
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/print/printcmd.py131
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/print/user_prompts.py108
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/refresh/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/refresh/refresh.py108
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/refresh/user_prompts.py189
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/release_actions/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/release_actions/conftest.py40
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/release_actions/release.py23
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/release_actions/sequence.py66
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/release_actions/support/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/release_actions/support/refine.py24
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/send_alert_text/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/send_alert_text/conftest.py24
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/send_alert_text/send.py94
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/set_timeouts/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/set_timeouts/set.py95
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/set_timeouts/user_prompts.py62
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/set_window_rect/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/set_window_rect/set.py459
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/set_window_rect/user_prompts.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/status/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/status/status.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/cross_origin.py63
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch.py125
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_number.py50
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_webelement.py100
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/switch.py101
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_window/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_window/alerts.py33
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/switch_to_window/switch.py100
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/__init__.py10
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/iframe.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/screenshot.py100
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/user_prompts.py121
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_screenshot/__init__.py21
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_screenshot/iframe.py54
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_screenshot/screenshot.py34
-rw-r--r--testing/web-platform/tests/webdriver/tests/classic/take_screenshot/user_prompts.py113
-rw-r--r--testing/web-platform/tests/webdriver/tests/conftest.py5
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/__init__.py12
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/asserts.py231
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/defaults.py6
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/fixtures.py489
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py530
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/fixtures_http.py240
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/helpers.py273
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/beforeunload.html16
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/default.html7
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/deleteframe.html6
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/frames.html16
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/frames_no_bfcache.html18
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/meta-utf8-after-1024-bytes.html17
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/render.html68
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/subframe.html16
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/test_actions.html213
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/test_actions_pointer.html102
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html139
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_handlers/__init__.py0
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_handlers/authentication.py37
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_handlers/cached.py14
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_handlers/headers.py22
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_handlers/must-revalidate.py17
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_handlers/redirect.py19
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_handlers/status.py16
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/http_request.py40
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/image.py40
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/inline.py72
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/keys.py904
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/screenshot.py50
-rw-r--r--testing/web-platform/tests/webdriver/tests/support/sync.py279
592 files changed, 55149 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webdriver/META.yml b/testing/web-platform/tests/webdriver/META.yml
new file mode 100644
index 0000000000..806ba3dec3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/META.yml
@@ -0,0 +1,10 @@
+spec: https://w3c.github.io/webdriver/
+suggested_reviewers:
+ - AutomatedTester
+ - bwalderman
+ - jgraham
+ - jrandolf
+ - juliandescottes
+ - sadym-chromium
+ - shs96c
+ - whimboo
diff --git a/testing/web-platform/tests/webdriver/README.md b/testing/web-platform/tests/webdriver/README.md
new file mode 100644
index 0000000000..67bb294d6e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/README.md
@@ -0,0 +1,17 @@
+# WebDriver specification tests
+
+Herein lies a set of conformance tests
+for the W3C web browser automation specification
+known as [WebDriver](http://w3c.github.io/webdriver/).
+The purpose of these tests is determine implementation compliance
+so that different driver implementations can determine
+whether they meet the recognized standard.
+
+## Chapters of the Spec that still need tests
+
+We are using a [tracking spreadsheet](https://docs.google.com/spreadsheets/d/1GUK_sdY2cv59VAJNDxZQIfypnOpapSQhMjfcJ9Wc42U/edit#gid=0)
+to coordinate work on these tests. Please look there to see who
+is working on what, and which areas are currently under-tested.
+
+The spec contributors and editors can frequently be found on the W3C
+#webdriver IRC channel.
diff --git a/testing/web-platform/tests/webdriver/tests/__init__.py b/testing/web-platform/tests/webdriver/tests/__init__.py
new file mode 100644
index 0000000000..0ba172ff2e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/__init__.py
@@ -0,0 +1,4 @@
+import pytest
+
+# Enable pytest assert introspection for assertion helper
+pytest.register_assert_rewrite('tests.support.asserts')
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/WEB_FEATURES.yml b/testing/web-platform/tests/webdriver/tests/bidi/WEB_FEATURES.yml
new file mode 100644
index 0000000000..a3af7470a9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: webdriver-bidi
+ files: "**"
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/__init__.py
new file mode 100644
index 0000000000..98b670f89f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/__init__.py
@@ -0,0 +1,168 @@
+from typing import Any, Callable, Dict, List, Mapping
+from webdriver.bidi.modules.script import ContextTarget
+
+
+# Compares 2 objects recursively.
+# Actual value can have more keys as part of the forwards-compat design.
+# Expected value can be a callable delegate, asserting the value.
+def recursive_compare(expected: Any, actual: Any) -> None:
+ if callable(expected):
+ expected(actual)
+ return
+
+ if isinstance(actual, List) and isinstance(expected, List):
+ assert len(expected) == len(actual)
+ for index, _ in enumerate(expected):
+ recursive_compare(expected[index], actual[index])
+ return
+
+ if isinstance(actual, Dict) and isinstance(expected, Dict):
+ # Actual Mapping can have more keys as part of the forwards-compat design.
+ assert (
+ expected.keys() <= actual.keys()
+ ), f"Key set should be present: {set(expected.keys()) - set(actual.keys())}"
+ for key in expected.keys():
+ recursive_compare(expected[key], actual[key])
+ return
+
+ assert expected == actual
+
+
+def any_bool(actual: Any) -> None:
+ assert isinstance(actual, bool)
+
+
+def any_dict(actual: Any) -> None:
+ assert isinstance(actual, dict)
+
+
+def any_int(actual: Any) -> None:
+ assert isinstance(actual, int)
+
+
+def any_int_or_null(actual: Any) -> None:
+ if actual is not None:
+ any_int(actual)
+
+
+def any_list(actual: Any) -> None:
+ assert isinstance(actual, list)
+
+
+def any_list_or_null(actual: Any) -> None:
+ if actual is not None:
+ any_list(actual)
+
+
+def any_string(actual: Any) -> None:
+ assert isinstance(actual, str)
+
+
+def any_string_or_null(actual: Any) -> None:
+ if actual is not None:
+ any_string(actual)
+
+
+def int_interval(start: int, end: int) -> Callable[[Any], None]:
+ def _(actual: Any) -> None:
+ any_int(actual)
+ assert start <= actual <= end
+
+ return _
+
+
+def assert_handle(obj: Mapping[str, Any], should_contain_handle: bool) -> None:
+ if should_contain_handle:
+ assert "handle" in obj, f"Result should contain `handle`. Actual: {obj}"
+ assert isinstance(obj["handle"], str), f"`handle` should be a string, but was {type(obj['handle'])}"
+
+ # Recursively check that handle is not found in any of the nested values.
+ if "value" in obj:
+ value = obj["value"]
+ if type(value) is list:
+ for v in value:
+ if type(v) is dict:
+ assert_handle(v, False)
+
+ if type(value) is dict:
+ for v in value.values():
+ if type(v) is dict:
+ assert_handle(v, False)
+
+ else:
+ assert "handle" not in obj, f"Result should not contain `handle`. Actual: {obj}"
+
+
+async def create_console_api_message(bidi_session, context: str, text: str):
+ await bidi_session.script.call_function(
+ function_declaration="""(text) => console.log(text)""",
+ arguments=[{"type": "string", "value": text}],
+ await_promise=False,
+ target=ContextTarget(context["context"]),
+ )
+ return text
+
+
+async def get_device_pixel_ratio(bidi_session, context: str) -> float:
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => {
+ return window.devicePixelRatio;
+ }""",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+ return result["value"]
+
+
+async def get_element_dimensions(bidi_session, context, element):
+ result = await bidi_session.script.call_function(
+ arguments=[element],
+ function_declaration="""(element) => {
+ const rect = element.getBoundingClientRect();
+ return { height: rect.height, width: rect.width }
+ }""",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ return remote_mapping_to_dict(result["value"])
+
+
+async def get_viewport_dimensions(bidi_session, context: str):
+ expression = """
+ ({
+ height: window.innerHeight || document.documentElement.clientHeight,
+ width: window.innerWidth || document.documentElement.clientWidth,
+ });
+ """
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ return remote_mapping_to_dict(result["value"])
+
+
+async def get_document_dimensions(bidi_session, context: str):
+ expression = """
+ ({
+ height: document.documentElement.scrollHeight,
+ width: document.documentElement.scrollWidth,
+ });
+ """
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ return remote_mapping_to_dict(result["value"])
+
+
+def remote_mapping_to_dict(js_object) -> Dict:
+ obj = {}
+ for key, value in js_object:
+ obj[key] = value["value"]
+
+ return obj
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/__init__.py
new file mode 100644
index 0000000000..e1327d55c9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/__init__.py
@@ -0,0 +1,35 @@
+from webdriver.bidi.modules.script import ContextTarget
+
+async def get_user_context_ids(bidi_session):
+ """
+ Returns the list of string ids of the current user contexts.
+ """
+ user_contexts = await bidi_session.browser.get_user_contexts()
+ return [user_context_info["userContext"] for user_context_info in user_contexts]
+
+
+async def set_local_storage(bidi_session, context: str, key: str, value: str):
+ """
+ Sets the value for the key in the context's localStorage.
+ """
+ await bidi_session.script.call_function(
+ function_declaration="""(key, value) => localStorage.setItem(key, value)""",
+ arguments=[{"type": "string", "value": key}, {"type": "string", "value": value}],
+ await_promise=False,
+ target=ContextTarget(context["context"]),
+ )
+
+
+async def get_local_storage(bidi_session, context: str, key: str):
+ """
+ Returns the value identified by the key from the context's localStorage.
+ """
+ result = await bidi_session.script.call_function(
+ function_declaration="""(key) => localStorage.getItem(key)""",
+ arguments=[{"type": "string", "value": key}],
+ await_promise=False,
+ target=ContextTarget(context["context"]),
+ )
+ if not "value" in result:
+ return None
+ return result["value"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/create_user_context.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/create_user_context.py
new file mode 100644
index 0000000000..f495498d07
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/create_user_context/create_user_context.py
@@ -0,0 +1,60 @@
+import pytest
+
+from .. import get_user_context_ids
+from .. import get_local_storage, set_local_storage
+
+
+@pytest.mark.asyncio
+async def test_create_context(bidi_session, create_user_context):
+ user_context = await create_user_context()
+ assert user_context in await get_user_context_ids(bidi_session)
+
+
+@pytest.mark.asyncio
+async def test_unique_id(bidi_session, create_user_context):
+ first_context = await create_user_context()
+ assert isinstance(first_context, str)
+
+ assert first_context in await get_user_context_ids(bidi_session)
+
+ other_context = await create_user_context()
+ assert isinstance(other_context, str)
+
+ assert first_context in await get_user_context_ids(bidi_session)
+ assert other_context in await get_user_context_ids(bidi_session)
+
+ assert first_context != other_context
+
+
+@pytest.mark.asyncio
+async def test_storage_isolation(bidi_session, create_user_context, inline):
+ first_context = await create_user_context()
+ other_context = await create_user_context()
+
+ test_key = "test"
+
+ tab_first_context = await bidi_session.browsing_context.create(
+ type_hint="tab",
+ user_context=first_context
+ )
+
+ await bidi_session.browsing_context.navigate(context=tab_first_context["context"],
+ url=inline("test"),
+ wait="complete")
+
+ tab_other_context = await bidi_session.browsing_context.create(
+ type_hint="tab",
+ user_context=other_context
+ )
+
+ await bidi_session.browsing_context.navigate(context=tab_other_context["context"],
+ url=inline("test"),
+ wait="complete")
+
+ assert await get_local_storage(bidi_session, tab_first_context, test_key) == None
+ assert await get_local_storage(bidi_session, tab_other_context, test_key) == None
+
+ await set_local_storage(bidi_session, tab_first_context, test_key, "value")
+
+ assert await get_local_storage(bidi_session, tab_first_context, test_key) == "value"
+ assert await get_local_storage(bidi_session, tab_other_context, test_key) == None
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/get_user_contexts.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/get_user_contexts.py
new file mode 100644
index 0000000000..b0f4d0e47c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/get_user_contexts/get_user_contexts.py
@@ -0,0 +1,45 @@
+import pytest
+
+from .. import get_user_context_ids
+
+
+@pytest.mark.asyncio
+async def test_default(bidi_session):
+ user_context_ids = await get_user_context_ids(bidi_session)
+
+ assert len(user_context_ids) > 0
+ assert "default" in user_context_ids
+
+
+@pytest.mark.asyncio
+async def test_create_remove_contexts(bidi_session, create_user_context):
+ # create two user contexts
+ user_context_1 = await create_user_context()
+ user_context_2 = await create_user_context()
+
+ user_context_ids = await get_user_context_ids(bidi_session)
+
+ # get_user_contexts should return at least 3 contexts:
+ # the default context and the 2 newly created contexts
+ assert len(user_context_ids) >= 3
+ assert user_context_1 in user_context_ids
+ assert user_context_2 in user_context_ids
+ assert "default" in user_context_ids
+
+ # remove user context 1
+ await bidi_session.browser.remove_user_context(user_context=user_context_1)
+
+ # assert that user context 1 is not returned by browser.getUserContexts
+ user_context_ids = await get_user_context_ids(bidi_session)
+ assert user_context_1 not in user_context_ids
+ assert user_context_2 in user_context_ids
+ assert "default" in user_context_ids
+
+ # remove user context 2
+ await bidi_session.browser.remove_user_context(user_context=user_context_2)
+
+ # assert that user context 2 is not returned by browser.getUserContexts
+ user_context_ids = await get_user_context_ids(bidi_session)
+ assert user_context_1 not in user_context_ids
+ assert user_context_2 not in user_context_ids
+ assert "default" in user_context_ids
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/invalid.py
new file mode 100644
index 0000000000..5e51499a2d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/invalid.py
@@ -0,0 +1,28 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_user_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browser.remove_user_context(user_context=value)
+
+
+async def test_params_user_context_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browser.remove_user_context(user_context="default")
+
+
+async def test_params_user_context_no_such_user_context(bidi_session):
+ with pytest.raises(error.NoSuchUserContextException):
+ await bidi_session.browser.remove_user_context(user_context="foo")
+
+
+async def params_user_context_removed_user_context(bidi_session):
+ user_context = await bidi_session.browser.create_user_context()
+ await bidi_session.browser.remove_user_context(user_context=user_context)
+
+ with pytest.raises(error.NoSuchUserContextException):
+ await bidi_session.browser.remove_user_context(user_context=user_context)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/user_context.py b/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/user_context.py
new file mode 100644
index 0000000000..98d6a2e2c8
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browser/remove_user_context/user_context.py
@@ -0,0 +1,75 @@
+import pytest
+
+from tests.support.sync import AsyncPoll
+import webdriver.bidi.error as error
+
+from .. import get_user_context_ids
+
+
+@pytest.mark.asyncio
+async def test_remove_context(bidi_session, create_user_context):
+ user_context = await create_user_context()
+ assert user_context in await get_user_context_ids(bidi_session)
+
+ await bidi_session.browser.remove_user_context(user_context=user_context)
+ assert user_context not in await get_user_context_ids(bidi_session)
+ assert "default" in await get_user_context_ids(bidi_session)
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+@pytest.mark.asyncio
+async def test_remove_context_closes_contexts(
+ bidi_session, subscribe_events, wait_for_event, create_user_context, type_hint
+):
+ # Subscribe to all browsing context events
+ await subscribe_events(events=["browsingContext.contextDestroyed"])
+
+ user_context_1 = await create_user_context()
+ user_context_2 = await create_user_context()
+
+ # context 1 and 2 are owned by user context 1
+ context_1 = await bidi_session.browsing_context.create(
+ user_context=user_context_1, type_hint=type_hint
+ )
+ context_2 = await bidi_session.browsing_context.create(
+ user_context=user_context_1, type_hint=type_hint
+ )
+ # context 3 and 4 are owned by user context 2
+ context_3 = await bidi_session.browsing_context.create(
+ user_context=user_context_2, type_hint=type_hint
+ )
+ context_4 = await bidi_session.browsing_context.create(
+ user_context=user_context_2, type_hint=type_hint
+ )
+
+ # Track all received browsingContext.contextDestroyed events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("browsingContext.contextDestroyed", on_event)
+
+ # destroy user context 1 and wait for context 1 and 2 to be destroyed
+ await bidi_session.browser.remove_user_context(user_context=user_context_1)
+
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+
+ assert len(events) == 2
+ destroyed_contexts = [event["context"] for event in events]
+ assert context_1["context"] in destroyed_contexts
+ assert context_2["context"] in destroyed_contexts
+
+ # destroy user context 1 and wait for context 3 and 4 to be destroyed
+ await bidi_session.browser.remove_user_context(user_context=user_context_2)
+
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 4)
+
+ assert len(events) == 4
+ destroyed_contexts = [event["context"] for event in events]
+ assert context_3["context"] in destroyed_contexts
+ assert context_4["context"] in destroyed_contexts
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/__init__.py
new file mode 100644
index 0000000000..dbacac8cf8
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/__init__.py
@@ -0,0 +1,97 @@
+from typing import Any, Mapping
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from .. import (
+ any_int,
+ any_string,
+ any_string_or_null,
+ recursive_compare,
+)
+
+
+def assert_browsing_context(
+ info,
+ context,
+ children=None,
+ is_root=True,
+ parent=None,
+ url=None,
+ user_context="default",
+):
+ assert "children" in info
+ if children is not None:
+ assert isinstance(info["children"], list)
+ assert len(info["children"]) == children
+ else:
+ assert info["children"] is None
+
+ assert "context" in info
+ assert isinstance(info["context"], str)
+ # Note: Only the tests for browsingContext.getTree should be allowed to
+ # pass None here because it's not possible to assert the exact browsing
+ # context id for frames.
+ if context is not None:
+ assert info["context"] == context
+
+ if is_root:
+ if parent is None:
+ # For a top-level browsing context there is no parent
+ assert info["parent"] is None
+ else:
+ assert "parent" in info
+ assert isinstance(info["parent"], str)
+ assert info["parent"] == parent
+ else:
+ # non root browsing context entries do not contain a parent
+ assert "parent" not in info
+ assert parent is None
+
+ assert "url" in info
+ assert isinstance(info["url"], str)
+ assert info["url"] == url
+ assert info["userContext"] == user_context
+
+
+def assert_navigation_info(event, expected_navigation_info):
+ recursive_compare(
+ {
+ "context": any_string,
+ "navigation": any_string_or_null,
+ "timestamp": any_int,
+ "url": any_string,
+ },
+ event,
+ )
+
+ if "context" in expected_navigation_info:
+ assert event["context"] == expected_navigation_info["context"]
+
+ if "navigation" in expected_navigation_info:
+ assert event["navigation"] == expected_navigation_info["navigation"]
+
+ if "timestamp" in expected_navigation_info:
+ expected_navigation_info["timestamp"](event["timestamp"])
+
+ if "url" in expected_navigation_info:
+ assert event["url"] == expected_navigation_info["url"]
+
+
+async def get_document_focus(bidi_session, context: Mapping[str, Any]) -> str:
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => {
+ return document.hasFocus();
+ }""",
+ target=ContextTarget(context["context"]),
+ await_promise=False)
+ return result["value"]
+
+
+async def get_visibility_state(bidi_session, context: Mapping[str, Any]) -> str:
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => {
+ return document.visibilityState;
+ }""",
+ target=ContextTarget(context["context"]),
+ await_promise=False)
+ return result["value"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/__init__.py
new file mode 100644
index 0000000000..5d0b52a5ac
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/__init__.py
@@ -0,0 +1,16 @@
+from typing import Any, Mapping
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+async def is_selector_focused(bidi_session, context: Mapping[str, Any], selector: str) -> bool:
+ result = await bidi_session.script.call_function(
+ function_declaration="""(selector) => {
+ return document.querySelector(selector) === document.activeElement;
+ }""",
+ arguments=[
+ {"type": "string", "value": selector},
+ ],
+ target=ContextTarget(context["context"]),
+ await_promise=False)
+ return result["value"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/activate.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/activate.py
new file mode 100644
index 0000000000..0abbbbac38
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/activate.py
@@ -0,0 +1,95 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+from . import is_selector_focused
+from .. import get_document_focus, get_visibility_state
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_activate(bidi_session, new_tab, top_context):
+ assert await get_document_focus(bidi_session, top_context) is False
+
+ await bidi_session.browsing_context.activate(context=top_context["context"])
+
+ assert await get_visibility_state(bidi_session, top_context) == 'visible'
+ assert await get_document_focus(bidi_session, top_context) is True
+
+
+async def test_deactivates_other_contexts(bidi_session, new_tab, top_context):
+ await bidi_session.browsing_context.activate(context=top_context["context"])
+
+ assert await get_visibility_state(bidi_session, top_context) == 'visible'
+ assert await get_document_focus(bidi_session, top_context) is True
+
+ assert await get_document_focus(bidi_session, new_tab) is False
+
+ await bidi_session.browsing_context.activate(context=new_tab["context"])
+
+ assert await get_document_focus(bidi_session, top_context) is False
+
+ assert await get_visibility_state(bidi_session, new_tab) == 'visible'
+ assert await get_document_focus(bidi_session, new_tab) is True
+
+
+async def test_keeps_focused_area(bidi_session, inline, new_tab, top_context):
+ await bidi_session.browsing_context.activate(context=new_tab["context"])
+ assert await get_visibility_state(bidi_session, new_tab) == 'visible'
+ assert await get_document_focus(bidi_session, new_tab) is True
+
+ await bidi_session.browsing_context.navigate(context=new_tab["context"],
+ url=inline("<textarea autofocus></textarea><input>"),
+ wait="complete")
+
+ await bidi_session.script.evaluate(
+ expression="""document.querySelector("input").focus()""",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False)
+
+ assert await is_selector_focused(bidi_session, new_tab, "input")
+
+ await bidi_session.browsing_context.activate(context=top_context["context"])
+ assert await get_document_focus(bidi_session, new_tab) is False
+ assert await is_selector_focused(bidi_session, new_tab, "input")
+
+ await bidi_session.browsing_context.activate(context=new_tab["context"])
+ assert await get_visibility_state(bidi_session, new_tab) == 'visible'
+ assert await get_document_focus(bidi_session, new_tab) is True
+ assert await is_selector_focused(bidi_session, new_tab, "input")
+
+
+async def test_double_activation(bidi_session, inline, new_tab):
+ await bidi_session.browsing_context.activate(context=new_tab["context"])
+ assert await get_visibility_state(bidi_session, new_tab) == 'visible'
+ assert await get_document_focus(bidi_session, new_tab) is True
+
+ await bidi_session.browsing_context.navigate(context=new_tab["context"],
+ url=inline("<input><script>document.querySelector('input').focus();</script>"),
+ wait="complete")
+ assert await is_selector_focused(bidi_session, new_tab, "input")
+
+ await bidi_session.browsing_context.activate(context=new_tab["context"])
+ assert await get_visibility_state(bidi_session, new_tab) == 'visible'
+ assert await get_document_focus(bidi_session, new_tab) is True
+ assert await is_selector_focused(bidi_session, new_tab, "input")
+
+ # Activate again.
+ await bidi_session.browsing_context.activate(context=new_tab["context"])
+ assert await get_visibility_state(bidi_session, new_tab) == 'visible'
+ assert await get_document_focus(bidi_session, new_tab) is True
+ assert await is_selector_focused(bidi_session, new_tab, "input")
+
+
+async def test_activate_window(bidi_session):
+ new_window_1 = await bidi_session.browsing_context.create(type_hint="window")
+ new_window_2 = await bidi_session.browsing_context.create(type_hint="window")
+
+ assert await get_visibility_state(bidi_session, new_window_2) == 'visible'
+ assert await get_document_focus(bidi_session, new_window_2) is True
+
+ assert await get_document_focus(bidi_session, new_window_1) is False
+
+ await bidi_session.browsing_context.activate(context=new_window_1["context"])
+
+ assert await get_visibility_state(bidi_session, new_window_1) == 'visible'
+ assert await get_document_focus(bidi_session, new_window_1) is True
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/invalid.py
new file mode 100644
index 0000000000..06a5dafa36
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/activate/invalid.py
@@ -0,0 +1,37 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.activate(
+ context=value
+ )
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_params_context_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.activate(
+ context=value
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_context_iframe(bidi_session, new_tab, get_test_page):
+ url = get_test_page(as_frame=True)
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete")
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ assert len(contexts) == 1
+ frames = contexts[0]["children"]
+ assert len(frames) == 1
+ frame_context = frames[0]["context"]
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.activate(context=frame_context)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/__init__.py
new file mode 100644
index 0000000000..32d44104d5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/__init__.py
@@ -0,0 +1,109 @@
+from math import floor
+from ... import (
+ get_device_pixel_ratio,
+ get_document_dimensions,
+ get_element_dimensions,
+ get_viewport_dimensions,
+ remote_mapping_to_dict,
+)
+
+from webdriver.bidi.modules.script import ContextTarget
+from webdriver.bidi.modules.browsing_context import ElementOptions
+
+
+async def get_element_coordinates(bidi_session, context, element):
+ """Get the coordinates of the element.
+
+ :param bidi_session: BiDiSession
+ :param context: Browsing context ID
+ :param element: Serialized element
+ :returns: Tuple of (int, int) containing element x, element y coordinates.
+ """
+ result = await bidi_session.script.call_function(
+ arguments=[element],
+ function_declaration="""(element) => {
+ const rect = element.getBoundingClientRect();
+ return { x: rect.x, y: rect.y }
+ }""",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+ value = remote_mapping_to_dict(result["value"])
+
+ return (value["x"], value["y"])
+
+
+async def get_page_y_offset(bidi_session, context):
+ """Get the window.pageYOffset of the context's viewport.
+
+ :param bidi_session: BiDiSession
+ :param context: Browsing context ID
+ :returns: int value of window.pageYOffset.
+ """
+ result = await bidi_session.script.evaluate(
+ expression="window.pageYOffset",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+ return result["value"]
+
+
+async def get_physical_element_dimensions(bidi_session, context, element):
+ """Get the physical dimensions of the element.
+
+ :param bidi_session: BiDiSession
+ :param context: Browsing context ID
+ :param element: Serialized element
+ :returns: Tuple of (int, int) containing element width, element height.
+ """
+ element_dimensions = await get_element_dimensions(bidi_session, context, element)
+ dpr = await get_device_pixel_ratio(bidi_session, context)
+ return (floor(element_dimensions["width"] * dpr), floor(element_dimensions["height"] * dpr))
+
+
+async def get_physical_viewport_dimensions(bidi_session, context):
+ """Get the physical dimensions of the context's viewport.
+
+ :param bidi_session: BiDiSession
+ :param context: Browsing context ID
+ :returns: Tuple of (int, int) containing viewport width, viewport height.
+ """
+ viewport = await get_viewport_dimensions(bidi_session, context)
+ dpr = await get_device_pixel_ratio(bidi_session, context)
+ return (floor(viewport["width"] * dpr), floor(viewport["height"] * dpr))
+
+
+async def get_physical_document_dimensions(bidi_session, context):
+ """Get the physical dimensions of the context's document.
+
+ :param bidi_session: BiDiSession
+ :param context: Browsing context ID
+ :returns: Tuple of (int, int) containing document width, document height.
+ """
+ document = await get_document_dimensions(bidi_session, context)
+ dpr = await get_device_pixel_ratio(bidi_session, context)
+ return (floor(document["width"] * dpr), floor(document["height"] * dpr))
+
+
+async def get_reference_screenshot(bidi_session, inline, context, html):
+ """Get the reference screenshot for the given context and html.
+
+ :param bidi_session: BiDiSession
+ :param context: Browsing context ID
+ :param html: Html string
+ :returns: Screenshot image.
+ """
+ url = inline(html)
+ await bidi_session.browsing_context.navigate(
+ context=context, url=url, wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(context),
+ )
+
+ return await bidi_session.browsing_context.capture_screenshot(
+ context=context,
+ clip=ElementOptions(element=element),
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py
new file mode 100644
index 0000000000..40497ce6ac
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py
@@ -0,0 +1,81 @@
+import pytest
+
+from math import floor
+from tests.support.image import png_dimensions
+
+from . import get_physical_viewport_dimensions
+from ... import get_device_pixel_ratio, get_viewport_dimensions
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("activate", [True, False],
+ ids=["with activate", "without activate"])
+async def test_capture(bidi_session, top_context, inline, compare_png_bidi,
+ activate):
+ expected_size = await get_physical_viewport_dimensions(bidi_session, top_context)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url="about:blank", wait="complete"
+ )
+ if activate:
+ await bidi_session.browsing_context.activate(
+ context=top_context["context"])
+ reference_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"])
+ assert png_dimensions(reference_data) == expected_size
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+ if activate:
+ await bidi_session.browsing_context.activate(
+ context=top_context["context"])
+ data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"])
+
+ comparison = await compare_png_bidi(data, reference_data)
+ assert not comparison.equal()
+
+ # Take a second screenshot that should be identical to validate that
+ # we don't just always return false here
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+ if activate:
+ await bidi_session.browsing_context.activate(
+ context=top_context["context"])
+ new_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"])
+
+ comparison = await compare_png_bidi(new_data, data)
+ assert comparison.equal()
+
+
+@pytest.mark.parametrize("delta_width", [-10, +20], ids=["width smaller", "width larger"])
+@pytest.mark.parametrize("delta_height", [-30, +40], ids=["height smaller", "height larger"])
+@pytest.mark.asyncio
+async def test_capture_with_viewport(bidi_session, new_tab, delta_width, delta_height):
+ original_viewport = await get_viewport_dimensions(bidi_session, new_tab)
+
+ dpr = await get_device_pixel_ratio(bidi_session, new_tab)
+
+ test_viewport = {
+ "width": original_viewport["width"] + delta_width,
+ "height": original_viewport["height"] + delta_height
+ }
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport)
+
+ expected_size = {
+ "width": floor(test_viewport["width"] * dpr),
+ "height": floor(test_viewport["height"] * dpr)
+ }
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url="about:blank", wait="complete"
+ )
+
+ result = await bidi_session.browsing_context.capture_screenshot(
+ context=new_tab["context"])
+ assert png_dimensions(result) == (expected_size["width"], expected_size["height"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py
new file mode 100644
index 0000000000..8300e962b9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py
@@ -0,0 +1,375 @@
+import pytest
+
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.browsing_context import ElementOptions, BoxOptions
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.image import png_dimensions
+
+
+from . import (
+ get_element_coordinates,
+ get_physical_element_dimensions,
+ get_reference_screenshot,
+)
+from ... import get_viewport_dimensions
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_clip_element(bidi_session, top_context, inline, compare_png_bidi):
+ url = inline("<input />")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('input')",
+ target=ContextTarget(top_context["context"]),
+ )
+ expected_size = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+ reference_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], clip=ElementOptions(element=element)
+ )
+ reference_data_dimensions = png_dimensions(reference_data)
+ assert reference_data_dimensions == expected_size
+
+ # Compare with the screenshot of the different element.
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(top_context["context"]),
+ )
+ data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], clip=ElementOptions(element=element)
+ )
+
+ assert png_dimensions(data) != reference_data_dimensions
+
+ # Take a second screenshot that should be identical to validate that
+ # we don't just always return false here.
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(top_context["context"]),
+ )
+ new_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], clip=ElementOptions(element=element)
+ )
+
+ comparison = await compare_png_bidi(new_data, data)
+ assert comparison.equal()
+
+
+async def test_clip_box(bidi_session, top_context, inline, compare_png_bidi):
+ url = inline("<input>")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('input')",
+ target=ContextTarget(top_context["context"]),
+ )
+ element_coordinates = await get_element_coordinates(
+ bidi_session, top_context, element
+ )
+ expected_size = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+ reference_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(
+ x=element_coordinates[0],
+ y=element_coordinates[1],
+ width=expected_size[0],
+ height=expected_size[1],
+ ),
+ )
+ reference_data_dimensions = png_dimensions(reference_data)
+ assert reference_data_dimensions == expected_size
+
+ # Compare with the screenshot of the different element.
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(top_context["context"]),
+ )
+ element_coordinates = await get_element_coordinates(
+ bidi_session, top_context, element
+ )
+ element_dimensions = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+ data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(
+ x=element_coordinates[0],
+ y=element_coordinates[1],
+ width=element_dimensions[0],
+ height=element_dimensions[1],
+ ),
+ )
+
+ assert png_dimensions(data) != reference_data_dimensions
+
+ # Take a second screenshot that should be identical to validate that
+ # we don't just always return false here.
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(top_context["context"]),
+ )
+ element_coordinates = await get_element_coordinates(
+ bidi_session, top_context, element
+ )
+ element_dimensions = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+ new_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(
+ x=element_coordinates[0],
+ y=element_coordinates[1],
+ width=element_dimensions[0],
+ height=element_dimensions[1],
+ ),
+ )
+
+ comparison = await compare_png_bidi(new_data, data)
+ assert comparison.equal()
+
+
+async def test_clip_box_scroll_to(bidi_session, top_context, inline, compare_png_bidi):
+ element_styles = "background-color: black; width: 50px; height:50px;"
+
+ # Render an element inside of viewport for the reference.
+ reference_data = await get_reference_screenshot(
+ bidi_session,
+ inline,
+ top_context["context"],
+ f"""<div style="{element_styles}"></div>""",
+ )
+
+ viewport_dimensions = await get_viewport_dimensions(bidi_session, top_context)
+
+ # Render the same element outside of viewport.
+ url = inline(
+ f"""<div style="{element_styles} margin-top: {viewport_dimensions["height"]}px"></div>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ element = await bidi_session.script.call_function(
+ await_promise=False,
+ function_declaration="""() => {{
+ const element = document.querySelector('div');
+
+ const rect = element.getBoundingClientRect();
+ // Scroll to have the element in the viewport.
+ window.scrollTo(0, rect.y);
+
+ return element;
+ }}""",
+ target=ContextTarget(top_context["context"]),
+ )
+ element_coordinates = await get_element_coordinates(
+ bidi_session, top_context, element
+ )
+ element_dimensions = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+ new_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(
+ x=element_coordinates[0],
+ y=element_coordinates[1],
+ width=element_dimensions[0],
+ height=element_dimensions[1],
+ ),
+ )
+
+ assert png_dimensions(new_data) == element_dimensions
+
+ comparison = await compare_png_bidi(reference_data, new_data)
+ assert comparison.equal()
+
+
+async def test_clip_box_partially_visible(
+ bidi_session, top_context, inline, compare_png_bidi
+):
+ viewport_dimensions = await get_viewport_dimensions(bidi_session, top_context)
+ element_styles = f"background-color: black; width: {viewport_dimensions['width']}px; height: 50px;"
+
+ # Render an element fully inside of viewport for the reference.
+ reference_data = await get_reference_screenshot(
+ bidi_session,
+ inline,
+ top_context["context"],
+ f"""<div style="{element_styles}"></div>""",
+ )
+
+ reference_data_dimensions = png_dimensions(reference_data)
+
+ element_styles = f"background-color: black; width: {viewport_dimensions['width'] + 100}px; height: 50px;"
+
+ url = inline(f"""<div style="{element_styles}"></div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(top_context["context"]),
+ )
+ element_coordinates = await get_element_coordinates(
+ bidi_session, top_context, element
+ )
+ expected_size = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+ new_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(
+ x=element_coordinates[0],
+ y=element_coordinates[1],
+ width=expected_size[0],
+ height=expected_size[1],
+ ),
+ )
+ new_data_dimensions = png_dimensions(new_data)
+
+ # Since the rendered element only partially visible,
+ # the screenshot dimensions will not be equal the element size.
+ assert new_data_dimensions != expected_size
+ assert new_data_dimensions == reference_data_dimensions
+
+ comparison = await compare_png_bidi(reference_data, new_data)
+ assert comparison.equal()
+
+
+@pytest.mark.parametrize("origin", ["document", "viewport"])
+async def test_clip_box_outside_of_window_viewport(
+ bidi_session, top_context, inline, compare_png_bidi, origin
+):
+ element_styles = "background-color: black; width: 50px; height:50px;"
+ viewport_dimensions = await get_viewport_dimensions(bidi_session, top_context)
+
+ # Render the element outside of viewport.
+ url = inline(
+ f"""<div style="{element_styles} margin-top: {viewport_dimensions["height"]}px"></div>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ element = await bidi_session.script.call_function(
+ await_promise=False,
+ function_declaration="""() => document.querySelector('div')""",
+ target=ContextTarget(top_context["context"]),
+ )
+ element_coordinates = await get_element_coordinates(
+ bidi_session, top_context, element
+ )
+ element_dimensions = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+
+ if origin == "viewport":
+ with pytest.raises(error.UnableToCaptureScreenException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(
+ x=element_coordinates[0],
+ y=element_coordinates[1],
+ width=element_dimensions[0],
+ height=element_dimensions[1],
+ ),
+ )
+ else:
+ data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(
+ x=element_coordinates[0],
+ y=element_coordinates[1],
+ width=element_dimensions[0],
+ height=element_dimensions[1],
+ ),
+ origin="document",
+ )
+ assert png_dimensions(data) == element_dimensions
+
+ # Render an element inside of viewport for the reference.
+ reference_data = await get_reference_screenshot(
+ bidi_session,
+ inline,
+ top_context["context"],
+ f"""<div style="{element_styles}"></div>""",
+ )
+
+ comparison = await compare_png_bidi(reference_data, data)
+ assert comparison.equal()
+
+
+@pytest.mark.parametrize("origin", ["document", "viewport"])
+async def test_clip_element_outside_of_window_viewport(
+ bidi_session, top_context, inline, compare_png_bidi, origin
+):
+ viewport_dimensions = await get_viewport_dimensions(bidi_session, top_context)
+
+ element_styles = "background-color: black; width: 50px; height:50px;"
+ # Render element outside of viewport.
+ url = inline(
+ f"""<div style="{element_styles} margin-top: {viewport_dimensions["height"]}px"></div>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ element = await bidi_session.script.evaluate(
+ await_promise=False,
+ expression="document.querySelector('div')",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ if origin == "viewport":
+ with pytest.raises(error.UnableToCaptureScreenException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=ElementOptions(element=element),
+ )
+ else:
+ data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=ElementOptions(element=element),
+ origin="document",
+ )
+
+ expected_size = await get_physical_element_dimensions(
+ bidi_session, top_context, element
+ )
+ assert png_dimensions(data) == expected_size
+
+ # Render an element inside of viewport for the reference.
+ reference_data = await get_reference_screenshot(
+ bidi_session,
+ inline,
+ top_context["context"],
+ f"""<div style="{element_styles}"></div>""",
+ )
+
+ comparison = await compare_png_bidi(reference_data, data)
+ assert comparison.equal()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/format.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/format.py
new file mode 100644
index 0000000000..7401d94a3e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/format.py
@@ -0,0 +1,39 @@
+import pytest
+
+from webdriver.bidi.modules.browsing_context import FormatOptions
+
+
+@pytest.mark.asyncio
+async def test_format_type(bidi_session, top_context, inline):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=inline("<div style='margin-top:2000px'>foo</div>"),
+ wait="complete")
+
+ png_screenshot = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ format=FormatOptions(type="image/png"))
+ jpeg_screenshot = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ format=FormatOptions(type="image/jpeg"))
+
+ assert png_screenshot != jpeg_screenshot
+
+
+@pytest.mark.asyncio
+async def test_format_quality(bidi_session, top_context, inline):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=inline("<div style='margin-top:2000px'>foo</div>"),
+ wait="complete")
+
+ jpeg_quality_screenshot = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ format=FormatOptions(type="image/jpeg",quality=0.1))
+ jpeg_high_quality_screenshot = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ format=FormatOptions(type="image/jpeg",quality=1))
+
+ assert jpeg_quality_screenshot != jpeg_high_quality_screenshot
+
+ assert len(jpeg_high_quality_screenshot) > len(jpeg_quality_screenshot)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/frame.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/frame.py
new file mode 100644
index 0000000000..7ce18db7d6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/frame.py
@@ -0,0 +1,59 @@
+import pytest
+
+from tests.support.image import png_dimensions
+from tests.support.screenshot import (
+ DEFAULT_CONTENT,
+ INNER_IFRAME_STYLE,
+ OUTER_IFRAME_STYLE,
+ REFERENCE_CONTENT,
+ REFERENCE_STYLE,
+)
+
+from . import get_physical_viewport_dimensions
+
+
+@pytest.mark.asyncio
+async def test_iframe(bidi_session, top_context, inline, iframe):
+ viewport_size = await get_physical_viewport_dimensions(bidi_session, top_context)
+
+ iframe_content = f"{INNER_IFRAME_STYLE}{DEFAULT_CONTENT}"
+ url = inline(f"{OUTER_IFRAME_STYLE}{iframe(iframe_content)}")
+ await bidi_session.browsing_context.navigate(context=top_context["context"],
+ url=url,
+ wait="complete")
+ reference_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"])
+ assert png_dimensions(reference_data) == viewport_size
+
+ all_contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context = all_contexts[0]["children"][0]
+
+ data = await bidi_session.browsing_context.capture_screenshot(context=frame_context["context"])
+
+ assert png_dimensions(data) < png_dimensions(reference_data)
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+@pytest.mark.asyncio
+async def test_context_origin(bidi_session, top_context, inline, iframe, compare_png_bidi, domain):
+ expected_size = await get_physical_viewport_dimensions(bidi_session, top_context)
+
+ initial_url = inline(f"{REFERENCE_STYLE}{REFERENCE_CONTENT}")
+ await bidi_session.browsing_context.navigate(context=top_context["context"],
+ url=initial_url,
+ wait="complete")
+
+ reference_data = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"])
+ assert png_dimensions(reference_data) == expected_size
+
+ iframe_content = f"{INNER_IFRAME_STYLE}{DEFAULT_CONTENT}"
+ new_url = inline(f"{OUTER_IFRAME_STYLE}{iframe(iframe_content, domain=domain)}")
+ await bidi_session.browsing_context.navigate(context=top_context["context"],
+ url=new_url,
+ wait="complete")
+
+ data = await bidi_session.browsing_context.capture_screenshot(context=top_context["context"])
+ comparison = await compare_png_bidi(data, reference_data)
+
+ assert comparison.equal()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/invalid.py
new file mode 100644
index 0000000000..6fef42a48f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/invalid.py
@@ -0,0 +1,149 @@
+import pytest
+
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.browsing_context import (
+ BoxOptions,
+ ElementOptions,
+ FormatOptions,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(context=value)
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_invalid_frame(bidi_session, value):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.capture_screenshot(context=value)
+
+
+async def test_closed_frame(bidi_session, top_context, inline, add_and_remove_iframe):
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ frame_id = await add_and_remove_iframe(top_context)
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.capture_screenshot(context=frame_id)
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_clip_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], clip=value
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_clip_type_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], clip={"type": value}
+ )
+
+
+async def test_params_clip_type_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], clip={"type": "foo"}
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_clip_element_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=ElementOptions(element=value),
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_clip_element_sharedId_invalid_type(
+ bidi_session, top_context, value
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=ElementOptions(element={"shareId": value}),
+ )
+
+
+async def test_params_clip_element_sharedId_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.NoSuchNodeException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=ElementOptions(element={"sharedId": "foo"}),
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, "foo", {}, []])
+async def test_params_clip_box_x_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(x=value, y=0, width=0, height=0),
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, "foo", {}, []])
+async def test_params_clip_box_y_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(x=0, y=value, width=0, height=0),
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, "foo", {}, []])
+async def test_params_clip_box_width_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(x=0, y=0, width=value, height=0),
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, "foo", {}, []])
+async def test_params_clip_box_height_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(x=0, y=0, width=0, height=value),
+ )
+
+
+async def test_params_clip_box_dimensions_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.UnableToCaptureScreenException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"],
+ clip=BoxOptions(x=0, y=0, width=0, height=0),
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, [], {}])
+async def test_params_origin_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], origin=value
+ )
+
+
+async def test_params_origin_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], origin="page"
+ )
+
+
+async def test_params_format_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], format=FormatOptions(type="image/invalid")
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/origin.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/origin.py
new file mode 100644
index 0000000000..7161d36336
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/origin.py
@@ -0,0 +1,56 @@
+import pytest
+
+from tests.support.image import png_dimensions
+
+from . import get_physical_document_dimensions, get_physical_viewport_dimensions
+
+
+@pytest.mark.asyncio
+async def test_origin(bidi_session, top_context, inline):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=inline("<div style='margin-top:2000px'>foo</div>"),
+ wait="complete",
+ )
+
+ viewport_dimensions = await get_physical_viewport_dimensions(
+ bidi_session, top_context
+ )
+ document_dimensions = await get_physical_document_dimensions(
+ bidi_session, top_context
+ )
+ assert not viewport_dimensions == document_dimensions
+
+ document_screenshot = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], origin="document"
+ )
+ viewport_screenshot = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], origin="viewport"
+ )
+
+ assert png_dimensions(document_screenshot) == document_dimensions
+ assert png_dimensions(viewport_screenshot) == viewport_dimensions
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("origin", ["document", "viewport"])
+async def test_origin_consistency(bidi_session, top_context, inline, origin):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=inline("<div style='margin-top:2000px'>foo</div>"),
+ wait="complete",
+ )
+ screenshot_a = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], origin=origin
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=inline("<div style='margin-top:2000px'>foo</div>"),
+ wait="complete",
+ )
+ screenshot_b = await bidi_session.browsing_context.capture_screenshot(
+ context=top_context["context"], origin=origin
+ )
+
+ assert screenshot_a == screenshot_b
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/window_handle.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/window_handle.py
new file mode 100644
index 0000000000..4f36fba197
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/classic_interop/window_handle.py
@@ -0,0 +1,7 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_top_level_context_id_equals_window_handle(top_context, current_session):
+ assert top_context["context"] == current_session.window_handle
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/close.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/close.py
new file mode 100644
index 0000000000..21bf7411e5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/close.py
@@ -0,0 +1,23 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("type_hint", ["window", "tab"])
+async def test_top_level_context(bidi_session, type_hint):
+ top_level_context = await bidi_session.browsing_context.create(
+ type_hint=type_hint
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree()
+ assert len(contexts) == 2
+
+ await bidi_session.browsing_context.close(context=top_level_context["context"])
+
+ contexts = await bidi_session.browsing_context.get_tree()
+ assert len(contexts) == 1
+
+ assert contexts[0]["context"] != top_level_context["context"]
+
+ # TODO: Add a test for closing the last tab once the behavior has been specified
+ # https://github.com/w3c/webdriver-bidi/issues/187
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/invalid.py
new file mode 100644
index 0000000000..7c73a83b13
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/close/invalid.py
@@ -0,0 +1,31 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.close(context=value)
+
+
+async def test_params_context_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.close(context="foo")
+
+
+async def test_child_context(bidi_session, test_page_same_origin_frame, top_context):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_same_origin_frame, wait="complete"
+ )
+
+ all_contexts = await bidi_session.browsing_context.get_tree()
+
+ assert len(all_contexts) == 1
+ parent_info = all_contexts[0]
+ assert len(parent_info["children"]) == 1
+ child_info = parent_info["children"][0]
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.close(context=child_info["context"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/context_created.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/context_created.py
new file mode 100644
index 0000000000..464d83a8aa
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_created/context_created.py
@@ -0,0 +1,265 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.bidi.modules.script import ContextTarget
+from webdriver.error import TimeoutException
+
+from .. import assert_browsing_context
+
+pytestmark = pytest.mark.asyncio
+
+CONTEXT_CREATED_EVENT = "browsingContext.contextCreated"
+
+
+async def test_not_unsubscribed(bidi_session):
+ await bidi_session.session.subscribe(events=[CONTEXT_CREATED_EVENT])
+ await bidi_session.session.unsubscribe(events=[CONTEXT_CREATED_EVENT])
+
+ # Track all received browsingContext.contextCreated events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_CREATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.create(type_hint="tab")
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_context(bidi_session, wait_for_event, wait_for_future_safe, subscribe_events, type_hint):
+ await subscribe_events([CONTEXT_CREATED_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_CREATED_EVENT)
+ top_level_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ context_info = await wait_for_future_safe(on_entry)
+
+ assert_browsing_context(
+ context_info,
+ top_level_context["context"],
+ children=None,
+ url="about:blank",
+ parent=None,
+ user_context="default"
+ )
+
+
+async def test_evaluate_window_open_without_url(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, top_context):
+ await subscribe_events([CONTEXT_CREATED_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_CREATED_EVENT)
+
+ await bidi_session.script.evaluate(
+ expression="""window.open();""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False)
+
+ context_info = await wait_for_future_safe(on_entry)
+
+ assert_browsing_context(
+ context_info,
+ context=None,
+ children=None,
+ url="about:blank",
+ parent=None,
+ )
+
+
+async def test_evaluate_window_open_with_url(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, inline, top_context):
+ url = inline("<div>foo</div>")
+
+ await subscribe_events([CONTEXT_CREATED_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_CREATED_EVENT)
+
+ await bidi_session.script.evaluate(
+ expression=f"""window.open("{url}");""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False)
+ context_info = await wait_for_future_safe(on_entry)
+
+ assert_browsing_context(
+ context_info,
+ context=None,
+ children=None,
+ url="about:blank",
+ parent=None,
+ )
+
+
+async def test_navigate_creates_iframes(bidi_session, subscribe_events, top_context, test_page_multiple_frames):
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_CREATED_EVENT, on_event)
+ await subscribe_events([CONTEXT_CREATED_EVENT])
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_multiple_frames, wait="complete"
+ )
+
+ wait = AsyncPoll(
+ bidi_session, message="Didn't receive context created events for frames"
+ )
+ await wait.until(lambda _: len(events) >= 2)
+ assert len(events) == 2
+
+ # Get all browsing contexts from the first tab
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ children_info = root_info["children"]
+ assert len(children_info) == 2
+
+ # Note: Live `browsingContext.contextCreated` events are always created with "about:blank":
+ # https://github.com/w3c/webdriver-bidi/issues/220#issuecomment-1145785349
+ assert_browsing_context(
+ events[0],
+ children_info[0]["context"],
+ children=None,
+ url="about:blank",
+ parent=root_info["context"],
+ )
+
+ assert_browsing_context(
+ events[1],
+ children_info[1]["context"],
+ children=None,
+ url="about:blank",
+ parent=root_info["context"],
+ )
+
+ remove_listener()
+
+
+async def test_navigate_creates_nested_iframes(bidi_session, subscribe_events, top_context, test_page_nested_frames):
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_CREATED_EVENT, on_event)
+ await subscribe_events([CONTEXT_CREATED_EVENT])
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_nested_frames, wait="complete"
+ )
+
+ wait = AsyncPoll(
+ bidi_session, message="Didn't receive context created events for frames"
+ )
+ await wait.until(lambda _: len(events) >= 2)
+ assert len(events) == 2
+
+ # Get all browsing contexts from the first tab
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ assert len(root_info["children"]) == 1
+ child1_info = root_info["children"][0]
+ assert len(child1_info["children"]) == 1
+ child2_info = child1_info["children"][0]
+
+ # Note: `browsingContext.contextCreated` is always created with "about:blank":
+ # https://github.com/w3c/webdriver-bidi/issues/220#issuecomment-1145785349
+ assert_browsing_context(
+ events[0],
+ child1_info["context"],
+ children=None,
+ url="about:blank",
+ parent=root_info["context"],
+ )
+
+ assert_browsing_context(
+ events[1],
+ child2_info["context"],
+ children=None,
+ url="about:blank",
+ parent=child1_info["context"],
+ )
+
+ remove_listener()
+
+
+async def test_subscribe_to_one_context(
+ bidi_session, subscribe_events, top_context, test_page_same_origin_frame
+):
+ # Subscribe to a specific context
+ await subscribe_events(
+ events=[CONTEXT_CREATED_EVENT], contexts=[top_context["context"]]
+ )
+
+ # Track all received browsingContext.contextCreated events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_CREATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.create(type_hint="tab")
+
+ # Make sure we didn't receive the event for the new tab
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_same_origin_frame, wait="complete"
+ )
+
+ # Make sure we received the event for the iframe
+ await wait.until(lambda _: len(events) >= 1)
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_user_context(
+ bidi_session,
+ wait_for_event,
+ wait_for_future_safe,
+ subscribe_events,
+ create_user_context,
+ type_hint,
+):
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_CREATED_EVENT, on_event)
+
+ await subscribe_events([CONTEXT_CREATED_EVENT])
+
+ user_context = await create_user_context()
+ assert len(events) == 0
+
+ on_entry = wait_for_event(CONTEXT_CREATED_EVENT)
+ context = await bidi_session.browsing_context.create(
+ type_hint=type_hint, user_context=user_context
+ )
+ context_info = await wait_for_future_safe(on_entry)
+
+ assert len(events) == 1
+
+ assert_browsing_context(
+ context_info,
+ context["context"],
+ children=None,
+ url="about:blank",
+ parent=None,
+ user_context=user_context,
+ )
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/context_destroyed.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/context_destroyed.py
new file mode 100644
index 0000000000..17f7acf2f9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/context_destroyed/context_destroyed.py
@@ -0,0 +1,292 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget
+from webdriver.error import TimeoutException
+
+from tests.support.sync import AsyncPoll
+from .. import assert_browsing_context
+
+pytestmark = pytest.mark.asyncio
+
+CONTEXT_DESTROYED_EVENT = "browsingContext.contextDestroyed"
+
+
+async def test_unsubscribe(bidi_session, new_tab):
+ await bidi_session.session.subscribe(events=[CONTEXT_DESTROYED_EVENT])
+ await bidi_session.session.unsubscribe(events=[CONTEXT_DESTROYED_EVENT])
+
+ # Track all received browsingContext.contextDestroyed events in the events array
+ events = []
+
+ async def on_event(_, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_DESTROYED_EVENT, on_event)
+
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_context(bidi_session, wait_for_event, wait_for_future_safe, subscribe_events, type_hint):
+ await subscribe_events([CONTEXT_DESTROYED_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_DESTROYED_EVENT)
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ await bidi_session.browsing_context.close(context=new_context["context"])
+
+ context_info = await wait_for_future_safe(on_entry)
+
+ assert_browsing_context(
+ context_info,
+ new_context["context"],
+ children=None,
+ url="about:blank",
+ parent=None,
+ user_context="default"
+ )
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_navigate(bidi_session, subscribe_events, new_tab, inline, domain):
+ await subscribe_events([CONTEXT_DESTROYED_EVENT])
+
+ # Track all received browsingContext.contextDestroyed events in the events array
+ events = []
+
+ async def on_event(_, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_DESTROYED_EVENT, on_event)
+
+ url = inline("<div>test</div>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ url=url, context=new_tab["context"], wait="complete"
+ )
+
+ # Make sure navigation doesn't cause the context to be destroyed
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_navigate_iframe(
+ bidi_session, wait_for_event, wait_for_future_safe, subscribe_events, new_tab, inline, domain
+):
+ await subscribe_events([CONTEXT_DESTROYED_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_DESTROYED_EVENT)
+
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>")
+ await bidi_session.browsing_context.navigate(
+ url=url, context=new_tab["context"], wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ frame = contexts[0]["children"][0]
+
+ # Navigate to destroy iframes
+ url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ url=url, context=new_tab["context"], wait="complete"
+ )
+
+ context_info = await wait_for_future_safe(on_entry)
+
+ assert_browsing_context(
+ context_info,
+ frame["context"],
+ children=None,
+ url=frame_url,
+ parent=new_tab["context"],
+ )
+
+
+async def test_delete_iframe(
+ bidi_session, wait_for_event, wait_for_future_safe, subscribe_events, new_tab, inline
+):
+ await subscribe_events([CONTEXT_DESTROYED_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_DESTROYED_EVENT)
+
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>")
+ await bidi_session.browsing_context.navigate(
+ url=url, context=new_tab["context"], wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ iframe = contexts[0]["children"][0]
+
+ # Delete the iframe
+ await bidi_session.script.evaluate(
+ expression="""document.querySelector('iframe').remove()""",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ context_info = await wait_for_future_safe(on_entry)
+
+ assert_browsing_context(
+ context_info,
+ iframe["context"],
+ children=None,
+ url=frame_url,
+ parent=new_tab["context"],
+ )
+
+
+async def test_delete_nested_iframes(
+ bidi_session,
+ subscribe_events,
+ new_tab,
+ test_page_nested_frames,
+ test_page_same_origin_frame,
+):
+ await subscribe_events([CONTEXT_DESTROYED_EVENT])
+ # Track all received browsingContext.contextDestroyed events in the events array
+ events = []
+
+ async def on_event(_, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_DESTROYED_EVENT, on_event)
+
+ await bidi_session.browsing_context.navigate(
+ url=test_page_nested_frames, context=new_tab["context"], wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ top_iframe = contexts[0]["children"][0]
+
+ # Delete top iframe
+ await bidi_session.script.evaluate(
+ expression="""document.querySelector('iframe').remove()""",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ assert len(events) == 1
+ assert_browsing_context(
+ events[0],
+ top_iframe["context"],
+ children=None,
+ url=test_page_same_origin_frame,
+ parent=new_tab["context"],
+ )
+
+ remove_listener()
+
+
+async def test_iframe_destroy_parent(
+ bidi_session, subscribe_events, new_tab, test_page_nested_frames
+):
+ await subscribe_events([CONTEXT_DESTROYED_EVENT])
+ # Track all received browsingContext.contextDestroyed events in the events array
+ events = []
+
+ async def on_event(_, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_DESTROYED_EVENT, on_event)
+
+ await bidi_session.browsing_context.navigate(
+ url=test_page_nested_frames, context=new_tab["context"], wait="complete"
+ )
+
+ # Destroy top context
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ assert len(events) == 1
+ assert_browsing_context(
+ events[0],
+ new_tab["context"],
+ children=None,
+ url=test_page_nested_frames,
+ parent=None,
+ )
+
+ remove_listener()
+
+
+async def test_subscribe_to_one_context(bidi_session, subscribe_events, new_tab):
+ # Subscribe to a specific context
+ await subscribe_events(
+ events=[CONTEXT_DESTROYED_EVENT], contexts=[new_tab["context"]]
+ )
+
+ # Track all received browsingContext.contextDestroyed events in the events array
+ events = []
+
+ async def on_event(_, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_DESTROYED_EVENT, on_event)
+
+ another_new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.browsing_context.close(context=another_new_tab["context"])
+
+ # Make sure we didn't receive the event for the new tab
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ # Make sure we received the event
+ await wait.until(lambda _: len(events) >= 1)
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_user_context(
+ bidi_session,
+ wait_for_event,
+ wait_for_future_safe,
+ subscribe_events,
+ create_user_context,
+ type_hint,
+):
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_DESTROYED_EVENT, on_event)
+
+ await subscribe_events([CONTEXT_DESTROYED_EVENT])
+
+ user_context = await create_user_context()
+ assert len(events) == 0
+
+ context = await bidi_session.browsing_context.create(
+ type_hint=type_hint, user_context=user_context
+ )
+ assert len(events) == 0
+
+ on_entry = wait_for_event(CONTEXT_DESTROYED_EVENT)
+ await bidi_session.browsing_context.close(context=context["context"])
+ context_info = await wait_for_future_safe(on_entry)
+ assert len(events) == 1
+
+ assert_browsing_context(
+ context_info,
+ context["context"],
+ children=None,
+ url="about:blank",
+ parent=None,
+ user_context=user_context,
+ )
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/background.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/background.py
new file mode 100644
index 0000000000..f1effe0537
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/background.py
@@ -0,0 +1,32 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+from .. import get_document_focus, get_visibility_state
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_background_default_false(bidi_session, type_hint):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ try:
+ assert await get_visibility_state(bidi_session, new_context) == "visible"
+ assert await get_document_focus(bidi_session, new_context) is True
+ finally:
+ await bidi_session.browsing_context.close(context=new_context["context"])
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+@pytest.mark.parametrize("background", [True, False])
+async def test_background(bidi_session, top_context, type_hint, background):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint, background=background)
+
+ try:
+ if background:
+ assert await get_visibility_state(bidi_session, top_context) == "visible"
+ else:
+ assert await get_visibility_state(bidi_session, new_context) == "visible"
+
+ assert await get_document_focus(bidi_session, new_context) != background
+ finally:
+ await bidi_session.browsing_context.close(context=new_context["context"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/invalid.py
new file mode 100644
index 0000000000..3f2cc886d0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/invalid.py
@@ -0,0 +1,90 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_reference_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.create(
+ type_hint="tab", reference_context=value
+ )
+
+
+async def test_params_reference_context_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.create(
+ type_hint="tab", reference_context="foo"
+ )
+
+
+async def test_params_reference_context_non_top_level(
+ bidi_session, test_page_same_origin_frame, top_context
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=test_page_same_origin_frame,
+ wait="complete",
+ )
+
+ all_contexts = await bidi_session.browsing_context.get_tree()
+
+ assert len(all_contexts) == 1
+ parent_info = all_contexts[0]
+ assert len(parent_info["children"]) == 1
+ child_info = parent_info["children"][0]
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.create(
+ type_hint="tab", reference_context=child_info["context"]
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_type_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.create(type_hint=value)
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_type_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.create(type_hint=value)
+
+
+@pytest.mark.parametrize("value", ['', 42, {}, []])
+async def test_params_background_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.create(type_hint="tab", background = value)
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_user_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.create(type_hint="tab", user_context=value)
+
+
+@pytest.mark.parametrize("value", ["", "unknown"])
+async def test_params_user_context_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchUserContextException):
+ await bidi_session.browsing_context.create(type_hint="tab", user_context=value)
+
+
+async def test_params_user_context_invalid_value_with_ref_context(bidi_session):
+ reference_context = await bidi_session.browsing_context.create(type_hint="tab")
+
+ with pytest.raises(error.NoSuchUserContextException):
+ await bidi_session.browsing_context.create(
+ reference_context=reference_context["context"],
+ type_hint="tab",
+ user_context="invalid",
+ )
+
+
+async def test_params_user_context_removed_context(bidi_session, create_user_context):
+ user_context = await create_user_context()
+ await bidi_session.browser.remove_user_context(user_context=user_context)
+
+ with pytest.raises(error.NoSuchUserContextException):
+ await bidi_session.browsing_context.create(type_hint="tab", user_context=user_context)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py
new file mode 100644
index 0000000000..6b7fd8b2be
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py
@@ -0,0 +1,66 @@
+import pytest
+
+from .. import assert_browsing_context
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", ["tab", "window"])
+async def test_reference_context(bidi_session, value):
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 1
+
+ reference_context = await bidi_session.browsing_context.create(type_hint="tab")
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 2
+
+ new_context = await bidi_session.browsing_context.create(
+ reference_context=reference_context["context"], type_hint=value
+ )
+ assert contexts[0]["context"] != new_context["context"]
+ assert contexts[0]["context"] != new_context["context"]
+
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 3
+
+ # Retrieve the new context info
+ contexts = await bidi_session.browsing_context.get_tree(
+ max_depth=0, root=new_context["context"]
+ )
+
+ assert_browsing_context(
+ contexts[0],
+ new_context["context"],
+ children=None,
+ is_root=True,
+ parent=None,
+ url="about:blank",
+ )
+
+ # We can not assert the specific behavior of reference_context here,
+ # so we only verify that a new browsing context was successfully created
+ # when a valid reference_context is provided.
+
+ await bidi_session.browsing_context.close(context=reference_context["context"])
+ await bidi_session.browsing_context.close(context=new_context["context"])
+
+
+@pytest.mark.parametrize("value", ["tab", "window"])
+async def test_reference_context_with_no_user_context_set(
+ bidi_session, value, create_user_context
+):
+ user_context = await create_user_context()
+
+ reference_context = await bidi_session.browsing_context.create(
+ type_hint="tab", user_context=user_context
+ )
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+
+ new_context = await bidi_session.browsing_context.create(
+ reference_context=reference_context["context"], type_hint=value
+ )
+ new_context_info = await bidi_session.browsing_context.get_tree(
+ max_depth=0, root=new_context["context"]
+ )
+
+ assert new_context_info[0]["userContext"] == user_context
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/type.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/type.py
new file mode 100644
index 0000000000..55ce7b4428
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/type.py
@@ -0,0 +1,41 @@
+import pytest
+
+from .. import assert_browsing_context
+from webdriver.bidi.modules.script import ContextTarget
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", ["tab", "window"])
+async def test_type(bidi_session, value):
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 1
+
+ new_context = await bidi_session.browsing_context.create(type_hint=value)
+ assert contexts[0]["context"] != new_context["context"]
+
+ # Check there is an additional browsing context
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 2
+
+ # Retrieve the new context info
+ contexts = await bidi_session.browsing_context.get_tree(
+ max_depth=0, root=new_context["context"]
+ )
+
+ assert_browsing_context(
+ contexts[0],
+ new_context["context"],
+ children=None,
+ is_root=True,
+ parent=None,
+ url="about:blank",
+ )
+
+ opener_protocol_value = await bidi_session.script.evaluate(
+ expression="!!window.opener",
+ target=ContextTarget(new_context["context"]),
+ await_promise=False)
+ assert opener_protocol_value["value"] is False
+
+ await bidi_session.browsing_context.close(context=new_context["context"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/user_context.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/user_context.py
new file mode 100644
index 0000000000..51406262fc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/create/user_context.py
@@ -0,0 +1,118 @@
+import pytest
+
+from .. import assert_browsing_context
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_user_context(bidi_session, type_hint, create_user_context):
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 1
+
+ user_context = await create_user_context()
+
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 1
+
+ new_context = await bidi_session.browsing_context.create(
+ user_context=user_context, type_hint=type_hint
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(max_depth=0)
+ assert len(contexts) == 2
+
+ assert_browsing_context(
+ contexts[1],
+ new_context["context"],
+ children=None,
+ is_root=True,
+ parent=None,
+ url="about:blank",
+ user_context=user_context,
+ )
+
+
+async def test_user_context_default(bidi_session, create_user_context):
+ user_context = await create_user_context()
+
+ # Create a browsing context with userContext set to "default"
+ context_1 = await bidi_session.browsing_context.create(
+ type_hint="tab", user_context="default"
+ )
+ context_tree_1 = await bidi_session.browsing_context.get_tree(
+ max_depth=0, root=context_1["context"]
+ )
+ assert_browsing_context(
+ context_tree_1[0],
+ context_1["context"],
+ url="about:blank",
+ user_context="default",
+ )
+
+ # Create a browsing context with no userContext parameter
+ context_2 = await bidi_session.browsing_context.create(
+ type_hint="tab",
+ )
+ context_tree_2 = await bidi_session.browsing_context.get_tree(
+ max_depth=0, root=context_2["context"]
+ )
+ assert_browsing_context(
+ context_tree_2[0],
+ context_2["context"],
+ url="about:blank",
+ user_context="default",
+ )
+
+
+async def test_overrides_user_context_from_reference_context(
+ bidi_session, create_user_context
+):
+ user_context_1 = await create_user_context()
+ user_context_2 = await create_user_context()
+
+ reference_context = await bidi_session.browsing_context.create(
+ type_hint="tab", user_context=user_context_1
+ )
+ reference_context_info = await bidi_session.browsing_context.get_tree(
+ max_depth=0, root=reference_context["context"]
+ )
+ assert reference_context_info[0]["userContext"] == user_context_1
+
+ new_context = await bidi_session.browsing_context.create(
+ reference_context=reference_context["context"],
+ type_hint="tab",
+ user_context=user_context_2,
+ )
+ new_context_info = await bidi_session.browsing_context.get_tree(
+ max_depth=0, root=new_context["context"]
+ )
+ assert new_context_info[0]["userContext"] == user_context_2
+
+
+async def test_user_context_nested_iframes(
+ bidi_session, create_user_context, new_tab, test_page_nested_frames
+):
+ user_context = await create_user_context()
+
+ new_context = await bidi_session.browsing_context.create(
+ user_context=user_context, type_hint="tab"
+ )
+
+ # Navigate the user context tab to a page with iframes.
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"], url=test_page_nested_frames, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_context["context"])
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+
+ # Check that iframes have the same user context as the parent.
+ assert len(root_info["children"]) == 1
+ child1_info = root_info["children"][0]
+ assert child1_info["userContext"] == user_context
+ assert len(child1_info["children"]) == 1
+ child2_info = child1_info["children"][0]
+ assert child2_info["userContext"] == user_context
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/dom_content_loaded.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/dom_content_loaded.py
new file mode 100644
index 0000000000..00cdad1dbb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/dom_content_loaded/dom_content_loaded.py
@@ -0,0 +1,195 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import int_interval
+from .. import assert_navigation_info
+
+pytestmark = pytest.mark.asyncio
+
+DOM_CONTENT_LOADED_EVENT = "browsingContext.domContentLoaded"
+
+
+async def test_unsubscribe(bidi_session, inline, top_context):
+ # test
+ await bidi_session.session.subscribe(events=[DOM_CONTENT_LOADED_EVENT])
+ await bidi_session.session.unsubscribe(events=[DOM_CONTENT_LOADED_EVENT])
+
+ # Track all received browsingContext.domContentLoaded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ DOM_CONTENT_LOADED_EVENT, on_event
+ )
+
+ url = inline("<div>foo</div>")
+
+ # When navigation reaches complete state,
+ # we should have received a browsingContext.domContentLoaded event
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ assert len(events) == 0
+
+ remove_listener()
+
+
+async def test_subscribe(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[DOM_CONTENT_LOADED_EVENT])
+
+ on_entry = wait_for_event(DOM_CONTENT_LOADED_EVENT)
+ url = inline("<div>foo</div>")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event,
+ {
+ "context": new_tab["context"],
+ "url": url,
+ "navigation": result["navigation"],
+ },
+ )
+
+
+async def test_timestamp(
+ bidi_session, current_time, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[DOM_CONTENT_LOADED_EVENT])
+
+ time_start = await current_time()
+
+ on_entry = wait_for_event(DOM_CONTENT_LOADED_EVENT)
+ url = inline("<div>foo</div>")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ time_end = await current_time()
+
+ assert_navigation_info(
+ event,
+ {
+ "context": new_tab["context"],
+ "navigation": result["navigation"],
+ "timestamp": int_interval(time_start, time_end),
+ },
+ )
+
+
+async def test_iframe(
+ bidi_session, subscribe_events, new_tab, test_page, test_page_same_origin_frame
+):
+ events = []
+
+ async def on_event(method, data):
+ # Filter out events for about:blank to avoid browser differences
+ if data["url"] != "about:blank":
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ DOM_CONTENT_LOADED_EVENT, on_event
+ )
+ await subscribe_events(events=[DOM_CONTENT_LOADED_EVENT])
+
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page_same_origin_frame
+ )
+
+ wait = AsyncPoll(
+ bidi_session, message="Didn't receive dom content loaded events for frames"
+ )
+ await wait.until(lambda _: len(events) >= 2)
+ assert len(events) == 2
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ assert len(root_info["children"]) == 1
+ child_info = root_info["children"][0]
+
+ # The ordering of the domContentLoaded event is not guaranteed between the
+ # root page and the iframe, find the appropriate events in the current list.
+ first_is_root = events[0]["context"] == root_info["context"]
+ root_event = events[0] if first_is_root else events[1]
+ child_event = events[1] if first_is_root else events[0]
+
+ assert_navigation_info(
+ root_event,
+ {
+ "context": root_info["context"],
+ "url": test_page_same_origin_frame,
+ "navigation": result["navigation"],
+ },
+ )
+ assert_navigation_info(
+ child_event, {"context": child_info["context"], "url": test_page}
+ )
+ assert child_event["navigation"] is not None
+ assert child_event["navigation"] != root_event["navigation"]
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_context(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, type_hint):
+ await subscribe_events(events=[DOM_CONTENT_LOADED_EVENT])
+
+ on_entry = wait_for_event(DOM_CONTENT_LOADED_EVENT)
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event, {"context": new_context["context"], "url": "about:blank"}
+ )
+ assert event["navigation"] is not None
+
+
+async def test_document_write(
+ bidi_session, subscribe_events, inline, top_context, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[DOM_CONTENT_LOADED_EVENT])
+
+ on_entry = wait_for_event(DOM_CONTENT_LOADED_EVENT)
+
+ await bidi_session.script.evaluate(
+ expression="""document.open(); document.write("<h1>Replaced</h1>"); document.close();""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event,
+ {"context": top_context["context"]},
+ )
+ assert event["navigation"] is not None
+
+
+async def test_page_with_base_tag(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[DOM_CONTENT_LOADED_EVENT])
+
+ on_entry = wait_for_event(DOM_CONTENT_LOADED_EVENT)
+ url = inline("""<base href="/relative-path">""")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event,
+ {"context": new_tab["context"], "navigation": result["navigation"], "url": url},
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/fragment_navigated.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/fragment_navigated.py
new file mode 100644
index 0000000000..a4bd012588
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/fragment_navigated.py
@@ -0,0 +1,311 @@
+import pytest
+
+from tests.support.sync import AsyncPoll
+from webdriver.bidi.modules.script import ContextTarget
+from webdriver.error import TimeoutException
+
+from ... import any_int, recursive_compare, int_interval
+from .. import assert_navigation_info
+
+pytestmark = pytest.mark.asyncio
+
+EMPTY_PAGE = "/webdriver/tests/bidi/support/empty.html"
+FRAGMENT_NAVIGATED_EVENT = "browsingContext.fragmentNavigated"
+
+
+async def test_unsubscribe(bidi_session, url, top_context):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url(EMPTY_PAGE), wait="complete"
+ )
+
+ await bidi_session.session.subscribe(events=[FRAGMENT_NAVIGATED_EVENT])
+ await bidi_session.session.unsubscribe(events=[FRAGMENT_NAVIGATED_EVENT])
+
+ # Track all received browsingContext.fragmentNavigated events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ FRAGMENT_NAVIGATED_EVENT, on_event
+ )
+
+ # When navigation reaches complete state,
+ # we should have received a browsingContext.fragmentNavigated event
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url(EMPTY_PAGE + '#foo'), wait="complete"
+ )
+
+ assert len(events) == 0
+
+ remove_listener()
+
+
+async def test_subscribe(bidi_session, subscribe_events, url, new_tab, wait_for_event, wait_for_future_safe):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(EMPTY_PAGE), wait="complete"
+ )
+
+ await subscribe_events(events=[FRAGMENT_NAVIGATED_EVENT])
+
+ on_entry = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+ target_url = url(EMPTY_PAGE + '#foo')
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=target_url, wait="complete")
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(event, {"context": new_tab["context"], "url": target_url})
+
+
+async def test_timestamp(bidi_session, current_time, subscribe_events, url, new_tab, wait_for_event):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(EMPTY_PAGE), wait="complete"
+ )
+
+ await subscribe_events(events=[FRAGMENT_NAVIGATED_EVENT])
+
+ time_start = await current_time()
+
+ on_entry = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+ target_url = url(EMPTY_PAGE + '#foo')
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=target_url, wait="complete")
+ event = await wait_for_future_safe(on_entry)
+
+ time_end = await current_time()
+
+ assert_navigation_info(
+ event,
+ {"context": new_tab["context"], "timestamp": int_interval(time_start, time_end)}
+ )
+
+
+async def test_navigation_id(
+ bidi_session, new_tab, url, subscribe_events, wait_for_event, wait_for_future_safe
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(EMPTY_PAGE), wait="complete"
+ )
+
+ await subscribe_events([FRAGMENT_NAVIGATED_EVENT])
+
+ on_frame_navigated = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+
+ target_url = url(EMPTY_PAGE + '#foo')
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=target_url, wait="complete")
+
+ recursive_compare(
+ {
+ 'context': new_tab["context"],
+ 'navigation': result["navigation"],
+ 'timestamp': any_int,
+ 'url': target_url
+ },
+ await wait_for_future_safe(on_frame_navigated),
+ )
+
+
+async def test_url_with_base_tag(bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe):
+ url = inline("""<base href="/relative-path">""")
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=url, wait="complete")
+
+ await subscribe_events(events=[FRAGMENT_NAVIGATED_EVENT])
+
+ on_frame_navigated = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+
+ target_url = url + '#foo'
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=target_url, wait="complete")
+
+ recursive_compare(
+ {
+ 'context': new_tab["context"],
+ 'url': target_url
+ },
+ await wait_for_future_safe(on_frame_navigated),
+ )
+
+
+async def test_iframe(
+ bidi_session, new_tab, url, inline, subscribe_events, wait_for_event, wait_for_future_safe
+):
+ initial_url = url(EMPTY_PAGE + '#foo')
+ parent_url = inline(f"<iframe src='{initial_url}'></iframe>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=parent_url, wait="complete"
+ )
+ all_contexts = await bidi_session.browsing_context.get_tree()
+
+ # about:blank + a new tab are top-level contexts.
+ assert len(all_contexts) == 2
+ parent_info = all_contexts[1]
+ assert len(parent_info["children"]) == 1
+ child_info = parent_info["children"][0]
+
+ await subscribe_events([FRAGMENT_NAVIGATED_EVENT])
+
+ on_frame_navigated = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+
+ target_url = url(EMPTY_PAGE + '#bar')
+ await bidi_session.browsing_context.navigate(
+ context=child_info["context"], url=target_url, wait="complete")
+
+ recursive_compare(
+ {
+ 'context': child_info["context"],
+ 'timestamp': any_int,
+ 'url': target_url
+ },
+ await wait_for_future_safe(on_frame_navigated),
+ )
+
+
+@pytest.mark.parametrize(
+ "hash_before, hash_after",
+ [
+ ("", "#foo"),
+ ("#foo", "#bar"),
+ ("#foo", "#foo"),
+ ]
+)
+async def test_document_location(
+ bidi_session, new_tab, url, subscribe_events, wait_for_event, wait_for_future_safe, hash_before, hash_after
+):
+ target_context = new_tab["context"]
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(EMPTY_PAGE + hash_before), wait="complete"
+ )
+
+ await subscribe_events([FRAGMENT_NAVIGATED_EVENT])
+
+ on_frame_navigated = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+
+ target_url = url(EMPTY_PAGE + hash_after)
+
+ await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="""(url) => {
+ document.location = url;
+ }""",
+ arguments=[
+ {"type": "string", "value": target_url},
+ ],
+ await_promise=False,
+ target=ContextTarget(target_context),
+ )
+
+ recursive_compare(
+ {
+ 'context': target_context,
+ 'timestamp': any_int,
+ 'url': target_url
+ },
+ await wait_for_future_safe(on_frame_navigated),
+ )
+
+
+@pytest.mark.parametrize(
+ "hash_before, hash_after",
+ [
+ ("", "#foo"),
+ ("#foo", "#bar"),
+ ("#foo", "#foo"),
+ ]
+)
+async def test_browsing_context_navigate(
+ bidi_session, new_tab, url, subscribe_events, wait_for_event, wait_for_future_safe, hash_before, hash_after
+):
+ target_context = new_tab["context"]
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(EMPTY_PAGE + hash_before), wait="complete"
+ )
+
+ await subscribe_events([FRAGMENT_NAVIGATED_EVENT])
+
+ on_frame_navigated = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+
+ target_url = url(EMPTY_PAGE + hash_after)
+
+ await bidi_session.browsing_context.navigate(
+ context=target_context, url=target_url, wait="complete")
+
+ recursive_compare(
+ {
+ 'context': target_context,
+ 'timestamp': any_int,
+ 'url': target_url
+ },
+ await wait_for_future_safe(on_frame_navigated),
+ )
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_context(bidi_session, subscribe_events, type_hint):
+ await subscribe_events(events=[FRAGMENT_NAVIGATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(FRAGMENT_NAVIGATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+async def test_document_write(bidi_session, subscribe_events, top_context):
+ await subscribe_events(events=[FRAGMENT_NAVIGATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(FRAGMENT_NAVIGATED_EVENT, on_event)
+
+ await bidi_session.script.evaluate(
+ expression="""document.open(); document.write("<h1>Replaced</h1>"); document.close();""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False
+ )
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+@pytest.mark.parametrize(
+ "before, after",
+ [
+ ("", "?foo"),
+ ("#foo", ""),
+ ]
+)
+async def test_regular_navigation(bidi_session, subscribe_events, url, new_tab, before, after):
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=url(EMPTY_PAGE) + before, wait="complete")
+
+ await subscribe_events(events=[FRAGMENT_NAVIGATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(FRAGMENT_NAVIGATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=url(EMPTY_PAGE + after), wait="complete")
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/history_api.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/history_api.py
new file mode 100644
index 0000000000..0af0a71c2b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/fragment_navigated/history_api.py
@@ -0,0 +1,57 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import any_int, recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+EMPTY_PAGE = "/webdriver/tests/bidi/support/empty.html"
+FRAGMENT_NAVIGATED_EVENT = "browsingContext.fragmentNavigated"
+
+
+@pytest.mark.parametrize(
+ "hash_before, hash_after",
+ [
+ ("", "#foo"),
+ ("#foo", "#bar"),
+ ("#foo", "#foo"),
+ ("#bar", ""),
+ ]
+)
+async def test_history_push_state(
+ bidi_session, new_tab, url, subscribe_events, wait_for_event,
+ wait_for_future_safe, hash_before, hash_after
+):
+ target_context = new_tab["context"]
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(EMPTY_PAGE + hash_before), wait="complete"
+ )
+
+ await subscribe_events([FRAGMENT_NAVIGATED_EVENT])
+
+ on_frame_navigated = wait_for_event(FRAGMENT_NAVIGATED_EVENT)
+
+ target_url = url(EMPTY_PAGE + hash_after)
+
+ await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="""(url) => {
+ history.pushState(null, null, url);
+ }""",
+ arguments=[
+ {"type": "string", "value": target_url},
+ ],
+ await_promise=False,
+ target=ContextTarget(target_context),
+ )
+
+ recursive_compare(
+ {
+ 'context': target_context,
+ 'timestamp': any_int,
+ 'url': target_url
+ },
+ await wait_for_future_safe(on_frame_navigated),
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py
new file mode 100644
index 0000000000..81c664740c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py
@@ -0,0 +1,183 @@
+import pytest
+
+from tests.support.sync import AsyncPoll
+from .. import assert_browsing_context
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_multiple_frames(
+ bidi_session,
+ top_context,
+ test_page,
+ test_page2,
+ test_page_multiple_frames,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_multiple_frames, wait="complete"
+ )
+
+ # First retrieve all browsing contexts of the first tab
+ top_level_context_id = top_context["context"]
+ all_contexts = await bidi_session.browsing_context.get_tree(root=top_level_context_id)
+
+ assert len(all_contexts) == 1
+ root_info = all_contexts[0]
+ assert_browsing_context(
+ root_info,
+ top_level_context_id,
+ children=2,
+ parent=None,
+ url=test_page_multiple_frames,
+ )
+
+ child1_info = root_info["children"][0]
+ assert_browsing_context(
+ child1_info,
+ context=None,
+ children=0,
+ is_root=False,
+ parent=None,
+ url=test_page,
+ )
+ assert child1_info["context"] != root_info["context"]
+
+ child2_info = root_info["children"][1]
+ assert_browsing_context(
+ child2_info,
+ context=None,
+ children=0,
+ is_root=False,
+ parent=None,
+ url=test_page2,
+ )
+ assert child2_info["context"] != root_info["context"]
+ assert child2_info["context"] != child1_info["context"]
+
+
+async def test_cross_origin(
+ bidi_session,
+ top_context,
+ test_page_cross_origin,
+ test_page_cross_origin_frame,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_cross_origin_frame, wait="complete"
+ )
+
+ # First retrieve all browsing contexts of the first tab
+ top_level_context_id = top_context["context"]
+ all_contexts = await bidi_session.browsing_context.get_tree(root=top_level_context_id)
+
+ assert len(all_contexts) == 1
+ root_info = all_contexts[0]
+ assert_browsing_context(
+ root_info,
+ top_level_context_id,
+ children=1,
+ parent=None,
+ url=test_page_cross_origin_frame,
+ )
+
+ child1_info = root_info["children"][0]
+ assert_browsing_context(
+ child1_info,
+ context=None,
+ children=0,
+ is_root=False,
+ parent=None,
+ url=test_page_cross_origin,
+ )
+ assert child1_info["context"] != root_info["context"]
+
+
+@pytest.mark.parametrize("user_context", ["default", "new"])
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_user_context(
+ bidi_session,
+ create_user_context,
+ subscribe_events,
+ wait_for_event,
+ inline,
+ user_context,
+ domain,
+):
+ await subscribe_events(["browsingContext.load"])
+
+ user_context_id = (
+ await create_user_context() if user_context == "new" else user_context
+ )
+
+ iframe_url_1 = inline("<div>foo</div>", domain=domain)
+ iframe_url_2 = inline("<div>bar</div>", domain=domain)
+ page_url = inline(
+ f"<iframe src='{iframe_url_1}'></iframe><iframe src='{iframe_url_2}'></iframe>"
+ )
+
+ context = await bidi_session.browsing_context.create(
+ type_hint="tab", user_context=user_context_id
+ )
+
+ # Record all load events.
+ events = []
+ async def on_event(method, data):
+ events.append(data)
+ remove_listener = bidi_session.add_event_listener("browsingContext.load", on_event)
+
+ await bidi_session.browsing_context.navigate(
+ context=context["context"], url=page_url, wait="complete"
+ )
+
+ # Wait until all iframes have been loaded.
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 3)
+
+ top_level_context_id = context["context"]
+ all_contexts = await bidi_session.browsing_context.get_tree(
+ root=top_level_context_id
+ )
+
+ assert len(all_contexts) == 1
+ root_info = all_contexts[0]
+ assert_browsing_context(
+ root_info,
+ top_level_context_id,
+ children=2,
+ parent=None,
+ url=page_url,
+ user_context=user_context_id,
+ )
+
+ # The contexts can be returned in any order, find the info matching iframe_url_1
+ child1_info = next(
+ filter(lambda x: x["url"] == iframe_url_1, root_info["children"]), None
+ )
+ assert child1_info is not None
+
+ assert_browsing_context(
+ child1_info,
+ context=None,
+ children=0,
+ is_root=False,
+ parent=None,
+ url=iframe_url_1,
+ user_context=user_context_id,
+ )
+ assert child1_info["context"] != root_info["context"]
+
+ child2_info = next(
+ filter(lambda x: x["url"] == iframe_url_2, root_info["children"]), None
+ )
+ assert child2_info is not None
+
+ assert_browsing_context(
+ child2_info,
+ context=None,
+ children=0,
+ is_root=False,
+ parent=None,
+ url=iframe_url_2,
+ user_context=user_context_id,
+ )
+ assert child2_info["context"] != root_info["context"]
+ assert child2_info["context"] != child1_info["context"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/invalid.py
new file mode 100644
index 0000000000..dbc93155e9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/invalid.py
@@ -0,0 +1,27 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [False, "foo", {}, []])
+async def test_params_max_depth_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.get_tree(max_depth=value)
+
+
+@pytest.mark.parametrize("value", [-1, 1.1, 2**53])
+async def test_params_max_depth_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.get_tree(max_depth=value)
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_root_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.get_tree(root=value)
+
+
+async def test_params_root_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.get_tree(root="foo")
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py
new file mode 100644
index 0000000000..ca1d0edfa1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py
@@ -0,0 +1,121 @@
+import pytest
+
+from .. import assert_browsing_context
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [0, 2**53 - 1])
+async def test_params_boundaries(bidi_session, value):
+ await bidi_session.browsing_context.get_tree(max_depth=value)
+
+
+async def test_null(
+ bidi_session,
+ top_context,
+ test_page,
+ test_page_same_origin_frame,
+ test_page_nested_frames,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_nested_frames, wait="complete"
+ )
+
+ # Retrieve browsing contexts for first tab only
+ top_level_context_id = top_context["context"]
+ contexts = await bidi_session.browsing_context.get_tree(root=top_level_context_id)
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ assert_browsing_context(
+ root_info,
+ top_level_context_id,
+ children=1,
+ parent=None,
+ url=test_page_nested_frames,
+ )
+
+ child1_info = root_info["children"][0]
+ assert_browsing_context(
+ child1_info,
+ context=None,
+ children=1,
+ is_root=False,
+ parent=None,
+ url=test_page_same_origin_frame,
+ )
+ assert child1_info["context"] != root_info["context"]
+
+ child2_info = child1_info["children"][0]
+ assert_browsing_context(
+ child2_info,
+ context=None,
+ children=0,
+ is_root=False,
+ parent=None,
+ url=test_page,
+ )
+ assert child2_info["context"] != root_info["context"]
+ assert child2_info["context"] != child1_info["context"]
+
+
+async def test_top_level_only(bidi_session, top_context, test_page_nested_frames):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_nested_frames, wait="complete"
+ )
+
+ # Retrieve browsing contexts for first tab only
+ top_level_context_id = top_context["context"]
+ contexts = await bidi_session.browsing_context.get_tree(
+ max_depth=0,
+ root=top_level_context_id
+ )
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ assert_browsing_context(
+ root_info,
+ top_level_context_id,
+ children=None,
+ parent=None,
+ url=test_page_nested_frames,
+ )
+
+
+async def test_top_level_and_one_child(
+ bidi_session,
+ top_context,
+ test_page_nested_frames,
+ test_page_same_origin_frame,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_nested_frames, wait="complete"
+ )
+
+ # Retrieve browsing contexts for first tab only
+ top_level_context_id = top_context["context"]
+ contexts = await bidi_session.browsing_context.get_tree(
+ max_depth=1,
+ root=top_level_context_id
+ )
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ assert_browsing_context(
+ root_info,
+ top_level_context_id,
+ children=1,
+ parent=None,
+ url=test_page_nested_frames,
+ )
+
+ child1_info = root_info["children"][0]
+ assert_browsing_context(
+ child1_info,
+ context=None,
+ children=None,
+ is_root=False,
+ parent=None,
+ url=test_page_same_origin_frame,
+ )
+ assert child1_info["context"] != root_info["context"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py
new file mode 100644
index 0000000000..74d11c6003
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py
@@ -0,0 +1,113 @@
+import pytest
+
+from .. import assert_browsing_context
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_null(bidi_session, top_context, test_page, type_hint):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page, wait="complete"
+ )
+
+ current_top_level_context_id = top_context["context"]
+ other_top_level_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ other_top_level_context_id = other_top_level_context["context"]
+
+ # Retrieve all top-level browsing contexts
+ contexts = await bidi_session.browsing_context.get_tree(root=None)
+
+ assert len(contexts) == 2
+ if contexts[0]["context"] == current_top_level_context_id:
+ current_info = contexts[0]
+ other_info = contexts[1]
+ else:
+ current_info = contexts[1]
+ other_info = contexts[0]
+
+ assert_browsing_context(
+ current_info,
+ current_top_level_context_id,
+ children=0,
+ parent=None,
+ url=test_page,
+ )
+
+ assert_browsing_context(
+ other_info,
+ other_top_level_context_id,
+ children=0,
+ parent=None,
+ url="about:blank",
+ )
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_top_level_context(bidi_session, top_context, test_page, type_hint):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page, wait="complete"
+ )
+
+ other_top_level_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ other_top_level_context_id = other_top_level_context["context"]
+ # Retrieve all browsing contexts of the newly opened tab/window
+ contexts = await bidi_session.browsing_context.get_tree(root=other_top_level_context_id)
+
+ assert len(contexts) == 1
+ assert_browsing_context(
+ contexts[0],
+ other_top_level_context_id,
+ children=0,
+ parent=None,
+ url="about:blank",
+ )
+
+
+async def test_child_context(
+ bidi_session,
+ top_context,
+ test_page_same_origin_frame,
+ test_page_nested_frames,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_nested_frames, wait="complete"
+ )
+
+ # First retrieve all browsing contexts for the first tab
+ top_level_context_id = top_context["context"]
+ all_contexts = await bidi_session.browsing_context.get_tree(root=top_level_context_id)
+
+ assert len(all_contexts) == 1
+ root_info = all_contexts[0]
+ assert_browsing_context(
+ root_info,
+ top_level_context_id,
+ children=1,
+ parent=None,
+ url=test_page_nested_frames,
+ )
+
+ child1_info = root_info["children"][0]
+ assert_browsing_context(
+ child1_info,
+ context=None,
+ children=1,
+ is_root=False,
+ parent=None,
+ url=test_page_same_origin_frame,
+ )
+
+ # Now retrieve all browsing contexts for the first browsing context child
+ child_contexts = await bidi_session.browsing_context.get_tree(root=child1_info["context"])
+
+ assert len(child_contexts) == 1
+ assert_browsing_context(
+ child_contexts[0],
+ root_info["children"][0]["context"],
+ children=1,
+ parent=root_info["context"],
+ url=test_page_same_origin_frame,
+ )
+
+ assert child1_info["children"][0] == child_contexts[0]["children"][0]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/handle_user_prompt.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/handle_user_prompt.py
new file mode 100644
index 0000000000..767305405c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/handle_user_prompt.py
@@ -0,0 +1,178 @@
+import asyncio
+import pytest
+
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.script import ContextTarget
+
+pytestmark = pytest.mark.asyncio
+
+USER_PROMPT_OPENED_EVENT = "browsingContext.userPromptOpened"
+
+
+async def test_alert(bidi_session, wait_for_event, wait_for_future_safe, top_context, subscribe_events):
+ await subscribe_events([USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ # Save as the task to await for it later.
+ task = asyncio.create_task(
+ bidi_session.script.evaluate(
+ expression="window.alert('test')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ )
+
+ # Wait for prompt to appear.
+ await wait_for_future_safe(on_entry)
+
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=top_context["context"]
+ )
+
+ # Make sure that script returned.
+ result = await task
+
+ assert result == {"type": "undefined"}
+
+
+@pytest.mark.parametrize("accept", [True, False])
+async def test_confirm(
+ bidi_session, wait_for_event, wait_for_future_safe, top_context, subscribe_events, accept
+):
+ await subscribe_events([USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ # Save as the task to await for it later.
+ task = asyncio.create_task(
+ bidi_session.script.evaluate(
+ expression="window.confirm('test')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ )
+
+ # Wait for prompt to appear.
+ await wait_for_future_safe(on_entry)
+
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=top_context["context"], accept=accept
+ )
+
+ # Check that return result of confirm is correct.
+ result = await task
+
+ assert result == {"type": "boolean", "value": accept}
+
+
+@pytest.mark.parametrize("accept", [True, False])
+async def test_prompt(
+ bidi_session, wait_for_event, wait_for_future_safe, top_context, subscribe_events, accept
+):
+ await subscribe_events([USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ # Save as the task to await for it later.
+ task = asyncio.create_task(
+ bidi_session.script.evaluate(
+ expression="window.prompt('Enter Your Name: ')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ )
+
+ # Wait for prompt to appear.
+ await wait_for_future_safe(on_entry)
+
+ test_user_text = "Test"
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=top_context["context"], accept=accept, user_text=test_user_text
+ )
+
+ # Check that return result of prompt is correct.
+ result = await task
+
+ if accept is True:
+ assert result == {"type": "string", "value": test_user_text}
+ else:
+ assert result == {"type": "null"}
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_two_top_level_contexts(
+ bidi_session, top_context, inline, subscribe_events, wait_for_event,
+ wait_for_future_safe, type_hint
+):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ await subscribe_events([USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=inline("<script>window.alert('test')</script>"),
+ )
+
+ # Wait for prompt to appear.
+ await wait_for_future_safe(on_entry)
+
+ # Try to close the prompt in another context.
+ with pytest.raises(error.NoSuchAlertException):
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=top_context["context"]
+ )
+
+ # Close the prompt in the correct context
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=new_context["context"]
+ )
+
+ await bidi_session.browsing_context.close(context=new_context["context"])
+
+
+async def test_multiple_frames(
+ bidi_session,
+ top_context,
+ inline,
+ test_page_multiple_frames,
+ subscribe_events,
+ wait_for_event,
+ wait_for_future_safe,
+):
+ await subscribe_events([USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=test_page_multiple_frames,
+ wait="complete",
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ assert len(contexts) == 1
+
+ assert len(contexts[0]["children"]) == 2
+ frame_1 = contexts[0]["children"][0]
+ frame_2 = contexts[0]["children"][1]
+
+ # Open a prompt in the first frame
+ await bidi_session.browsing_context.navigate(
+ context=frame_1["context"],
+ url=inline("<script>window.response = window.confirm('test')</script>"),
+ )
+
+ # Wait for prompt to appear.
+ await wait_for_future_safe(on_entry)
+
+ # Close prompt from the second frame.
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=frame_2["context"], accept=True
+ )
+
+ # Check that return result of confirm is correct.
+ result = await bidi_session.script.evaluate(
+ expression="window.response",
+ target=ContextTarget(frame_1["context"]),
+ await_promise=False,
+ )
+
+ assert result == {"type": "boolean", "value": True}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/invalid.py
new file mode 100644
index 0000000000..fd3c31a786
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/handle_user_prompt/invalid.py
@@ -0,0 +1,39 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.handle_user_prompt(context=value)
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_params_context_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.handle_user_prompt(context=value)
+
+
+@pytest.mark.parametrize("value", ["foo", 42, {}, []])
+async def test_params_accept_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=top_context["context"], accept=value
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_user_text_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=top_context["context"], user_text=value
+ )
+
+
+async def test_no_alert(bidi_session, top_context):
+ with pytest.raises(error.NoSuchAlertException):
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=top_context["context"]
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/load.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/load.py
new file mode 100644
index 0000000000..4c2c6e74c5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/load/load.py
@@ -0,0 +1,175 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.bidi.modules.script import ContextTarget
+from webdriver.error import TimeoutException
+
+from ... import int_interval
+from .. import assert_navigation_info
+
+pytestmark = pytest.mark.asyncio
+
+CONTEXT_LOAD_EVENT = "browsingContext.load"
+
+
+async def test_unsubscribe(bidi_session, inline, new_tab):
+ await bidi_session.session.subscribe(events=[CONTEXT_LOAD_EVENT])
+ await bidi_session.session.unsubscribe(events=[CONTEXT_LOAD_EVENT])
+
+ # Track all received browsingContext.load events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_LOAD_EVENT, on_event)
+
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+async def test_subscribe(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[CONTEXT_LOAD_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_LOAD_EVENT)
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=url)
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(event, {"context": new_tab["context"], "url": url})
+
+
+async def test_timestamp(
+ bidi_session, current_time, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[CONTEXT_LOAD_EVENT])
+
+ time_start = await current_time()
+
+ on_entry = wait_for_event(CONTEXT_LOAD_EVENT)
+ url = inline("<div>foo</div>")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ time_end = await current_time()
+
+ assert_navigation_info(
+ event,
+ {
+ "context": new_tab["context"],
+ "navigation": result["navigation"],
+ "timestamp": int_interval(time_start, time_end),
+ },
+ )
+
+
+async def test_iframe(
+ bidi_session, subscribe_events, new_tab, test_page, test_page_same_origin_frame
+):
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(CONTEXT_LOAD_EVENT, on_event)
+ await subscribe_events(events=[CONTEXT_LOAD_EVENT])
+
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page_same_origin_frame
+ )
+
+ wait = AsyncPoll(
+ bidi_session, message="Didn't receive context load events for frames"
+ )
+ await wait.until(lambda _: len(events) >= 2)
+ assert len(events) == 2
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ assert len(root_info["children"]) == 1
+ child_info = root_info["children"][0]
+
+ # First load event comes from iframe
+ assert_navigation_info(
+ events[0], {"context": child_info["context"], "url": test_page}
+ )
+ assert_navigation_info(
+ events[1],
+ {
+ "context": root_info["context"],
+ "navigation": result["navigation"],
+ "url": test_page_same_origin_frame,
+ },
+ )
+
+ assert events[0]["navigation"] is not None
+ assert events[0]["navigation"] != events[1]["navigation"]
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_context(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, type_hint):
+ await subscribe_events(events=[CONTEXT_LOAD_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_LOAD_EVENT)
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event, {"context": new_context["context"], "url": "about:blank"}
+ )
+ assert event["navigation"] is not None
+
+
+async def test_document_write(
+ bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[CONTEXT_LOAD_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_LOAD_EVENT)
+
+ await bidi_session.script.evaluate(
+ expression="""document.open(); document.write("<h1>Replaced</h1>"); document.close();""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event,
+ {"context": top_context["context"]},
+ )
+ assert event["navigation"] is not None
+
+
+async def test_page_with_base_tag(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[CONTEXT_LOAD_EVENT])
+
+ on_entry = wait_for_event(CONTEXT_LOAD_EVENT)
+ url = inline("""<base href="/relative-path">""")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event,
+ {"context": new_tab["context"], "navigation": result["navigation"], "url": url},
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/context.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/context.py
new file mode 100644
index 0000000000..49a6941486
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/context.py
@@ -0,0 +1,88 @@
+import pytest
+import webdriver.bidi.error as error
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.asyncio
+async def test_params_context_invalid_value(bidi_session, inline, top_context):
+ url = inline("""<div>foo</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.locate_nodes(
+ context="foo", locator={ "type": "css", "value": "div" }
+ )
+
+
+@pytest.mark.asyncio
+async def test_locate_in_different_contexts(bidi_session, inline, top_context, new_tab):
+ url = inline("""<div class="in-top-context">foo</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ # Try to locate nodes in the other context
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=new_tab["context"], locator={"type": "css", "value": ".in-top-context"}
+ )
+
+ assert result["nodes"] == []
+
+ # Locate in the correct context
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"], locator={"type": "css", "value": ".in-top-context"}
+ )
+
+ expected = [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"class": "in-top-context"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }
+ ]
+
+ recursive_compare(expected, result["nodes"])
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+@pytest.mark.asyncio
+async def test_locate_in_iframe(bidi_session, inline, top_context, domain):
+ iframe_url_1 = inline("<div id='in-iframe'>foo</div>", domain=domain)
+ page_url = inline(f"<iframe src='{iframe_url_1}'></iframe>")
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=page_url, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ iframe_context = contexts[0]["children"][0]
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=iframe_context["context"],
+ locator={"type": "css", "value": "#in-iframe"}
+ )
+
+ expected = [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "in-iframe"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }
+ ]
+
+ recursive_compare(expected, result["nodes"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py
new file mode 100644
index 0000000000..ff00de91ed
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/invalid.py
@@ -0,0 +1,228 @@
+import pytest
+import webdriver.bidi.error as error
+
+from webdriver.bidi.modules.script import ContextTarget
+
+pytestmark = pytest.mark.asyncio
+
+
+MAX_INT = 9007199254740991
+
+
+async def navigate_to_page(bidi_session, inline, top_context):
+ url = inline("""<div>foo</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=value, locator={"type": "css", "value": "div"}
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_locator_type_invalid_type(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"], locator={ "type": value, "value": "div" }
+ )
+
+
+@pytest.mark.parametrize("type", ["", "invalid"])
+async def test_params_locator_type_invalid_value(bidi_session, inline, top_context, type):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"], locator={ "type": type, "value": "div" }
+ )
+
+
+@pytest.mark.parametrize("type,value", [
+ ("css", "a*b"),
+ ("xpath", ""),
+ ("innerText", "")
+])
+async def test_params_locator_value_invalid_value(bidi_session, inline, top_context, type, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidSelectorException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"], locator={ "type": type, "value": value }
+ )
+
+
+async def test_params_locator_xpath_unknown_error(bidi_session, inline, top_context):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.UnknownErrorException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"], locator={"type": "xpath", "value": "/foo:bar"}
+ )
+
+
+@pytest.mark.parametrize("value", [False, "string", 1.5, {}, []])
+async def test_params_max_node_count_invalid_type(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div" },
+ max_node_count=value
+ )
+
+
+@pytest.mark.parametrize("value", [0, MAX_INT + 1])
+async def test_params_max_node_count_invalid_value(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "invalid", "value": "div" },
+ max_node_count=value
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_ownership_invalid_type(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div" },
+ ownership=value
+ )
+
+
+async def test_params_ownership_invalid_value(bidi_session, inline, top_context):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div" },
+ ownership="foo"
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_sandbox_invalid_type(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div" },
+ sandbox=value
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_serialization_options_invalid_type(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div" },
+ serialization_options=value
+ )
+
+
+@pytest.mark.parametrize("value", [False, "string", 42, {}])
+async def test_params_start_nodes_invalid_type(bidi_session, inline, top_context, value):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div" },
+ start_nodes=value
+ )
+
+
+async def test_params_start_nodes_empty_list(bidi_session, inline, top_context):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div" },
+ start_nodes=[]
+ )
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ {"type": "number", "value": 3},
+ {"type": "window"},
+ {"type": "array", "value": ["test"]},
+ {
+ "type": "object",
+ "value": [
+ ["1", {"type": "string", "value": "foo"}],
+ ],
+ },
+ ],
+)
+async def test_params_start_nodes_not_dom_node(
+ bidi_session, inline, top_context, value
+):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ if value["type"] == "window":
+ value["value"] = top_context["context"]
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={"type": "css", "value": "div"},
+ start_nodes=[value],
+ )
+
+
+@pytest.mark.parametrize(
+ "expression",
+ [
+ "document.querySelector('input#button').attributes[0]",
+ "document.querySelector('#with-text-node').childNodes[0]",
+ """document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")""",
+ "document.querySelector('#with-comment').childNodes[0]",
+ "document.doctype",
+ "document.getElementsByTagName('div')",
+ "document.querySelectorAll('div')"
+ ],
+)
+async def test_params_start_nodes_dom_node_not_element(
+ bidi_session, inline, top_context, get_test_page, expression
+):
+ await navigate_to_page(bidi_session, inline, top_context)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ remote_reference = await bidi_session.script.evaluate(
+ expression=expression,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={"type": "css", "value": "div"},
+ start_nodes=[remote_reference],
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py
new file mode 100644
index 0000000000..656eaddc1f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/locator.py
@@ -0,0 +1,207 @@
+import pytest
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.parametrize("type,value", [
+ ("css", "div"),
+ ("xpath", "//div"),
+ ("innerText", "foobarBARbaz")
+])
+@pytest.mark.asyncio
+async def test_find_by_locator(bidi_session, inline, top_context, type, value):
+ url = inline("""<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBARbaz</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": type, "value": value }
+ )
+
+ expected = [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }
+ ]
+
+ recursive_compare(expected, result["nodes"])
+
+
+@pytest.mark.parametrize("ignore_case,match_type,max_depth,value,expected", [
+ (True, "full", None, "bar", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "children": [],
+ "localName": "strong",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ ),
+ (False, "full", None, "BAR", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ ),
+ (True, "partial", None, "ba", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "strong",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ ),
+ (False, "partial", None, "ba", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ ),
+ (True, "full", 0, "foobarbarbaz", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 4,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ ),
+ (False, "full", 0, "foobarBARbaz", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 4,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ ),
+ (True, "partial", 0, "bar", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 4,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ ),
+ (False, "partial", 0, "BAR", [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 4,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]
+ )
+], ids=[
+ "ignore_case_true_full_match_no_max_depth",
+ "ignore_case_false_full_match_no_max_depth",
+ "ignore_case_true_partial_match_no_max_depth",
+ "ignore_case_false_partial_match_no_max_depth",
+ "ignore_case_true_full_match_max_depth_zero",
+ "ignore_case_false_full_match_max_depth_zero",
+ "ignore_case_true_partial_match_max_depth_zero",
+ "ignore_case_false_partial_match_max_depth_zero",
+])
+@pytest.mark.asyncio
+async def test_find_by_inner_text(bidi_session, inline, top_context, ignore_case, match_type, max_depth, value, expected):
+ url = inline("""<div>foo<span><strong>bar</strong></span><span>BAR</span>baz</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={
+ "type": "innerText",
+ "value": value,
+ "ignoreCase": ignore_case,
+ "matchType": match_type,
+ "maxDepth": max_depth
+ }
+ )
+
+ recursive_compare(expected, result["nodes"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py
new file mode 100644
index 0000000000..4652026e96
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/max_node_count.py
@@ -0,0 +1,181 @@
+import pytest
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.parametrize("type,value,max_count,expected", [
+ ("css", "div", 1, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("xpath", "//div", 1, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("innerText", "foo", 1, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("css", "div", 10, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("xpath", "//div", 10, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ ),
+ ("innerText", "foo", 10, [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ }]
+ )
+], ids=[
+ "css_single",
+ "xpath_single",
+ "inner_text_single",
+ "css_multiple",
+ "xpath_multiple",
+ "inner_text_multiple"
+])
+@pytest.mark.asyncio
+async def test_find_by_locator_limit_return_count(bidi_session, inline, top_context, type, value, max_count, expected):
+ url = inline("""<div data-class="one">foo</div><div data-class="two">foo</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": type, "value": value },
+ max_node_count = max_count
+ )
+
+ recursive_compare(expected, result["nodes"])
+
+
+@pytest.mark.asyncio
+async def test_several_context_nodes(bidi_session, inline, top_context):
+ url = inline(
+ """
+ <div class="context-node">
+ <div>should be returned</div>
+ </div>
+ <div class="context-node">
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ <div>should not be returned</div>
+ </div>
+ """
+ )
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result_context_nodes = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={"type": "css", "value": ".context-node"},
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={"type": "css", "value": "div"},
+ max_node_count=1,
+ start_nodes=[
+ {"sharedId": result_context_nodes["nodes"][0]["sharedId"]},
+ {"sharedId": result_context_nodes["nodes"][1]["sharedId"]},
+ ],
+ )
+
+ assert len(result["nodes"]) == 1
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/ownership.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/ownership.py
new file mode 100644
index 0000000000..b1830c740a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/ownership.py
@@ -0,0 +1,26 @@
+import pytest
+
+from ... import assert_handle
+
+
+@pytest.mark.parametrize("ownership,has_handle", [
+ ("root", True),
+ ("none", False)
+])
+@pytest.mark.asyncio
+async def test_root_ownership_of_located_nodes(bidi_session, inline, top_context, ownership, has_handle):
+ url = inline("""<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBARbaz</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" },
+ ownership=ownership
+ )
+
+ assert len(result["nodes"]) == 1
+ result_node = result["nodes"][0]
+
+ assert_handle(result_node, has_handle)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/sandbox.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/sandbox.py
new file mode 100644
index 0000000000..efa431bf19
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/sandbox.py
@@ -0,0 +1,111 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget,OwnershipModel
+
+
+@pytest.mark.asyncio
+async def test_locate_nodes_in_sandbox(bidi_session, inline, top_context):
+ url = inline("""<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBARbaz</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" },
+ sandbox="sandbox"
+ )
+
+ assert len(result["nodes"]) == 1
+ node_id = result["nodes"][0]["sharedId"]
+
+ # Since the node was found in the sandbox, it should be available
+ # to scripts running in the sandbox.
+ result_in_sandbox = await bidi_session.script.call_function(
+ function_declaration="function(){ return arguments[0]; }",
+ target=ContextTarget(top_context["context"], "sandbox"),
+ await_promise=True,
+ arguments=[
+ {
+ "sharedId": node_id
+ }
+ ]
+ )
+ assert result_in_sandbox["type"] == "node"
+ assert result_in_sandbox["sharedId"] == node_id
+
+
+@pytest.mark.asyncio
+async def test_locate_same_node_in_different_sandboxes_returns_same_id(bidi_session, inline, top_context):
+ url = inline("""<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBARbaz</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ first_result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" },
+ sandbox="first_sandbox"
+ )
+
+ assert len(first_result["nodes"]) == 1
+
+ second_result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" },
+ sandbox="second_sandbox"
+ )
+ assert len(second_result["nodes"]) == 1
+ assert first_result["nodes"][0]["sharedId"] == second_result["nodes"][0]["sharedId"]
+
+
+@pytest.mark.asyncio
+async def test_locate_same_node_in_default_sandbox_returns_same_id_as_sandbox(bidi_session, inline, top_context):
+ url = inline("""<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBARbaz</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" }
+ )
+
+ assert len(result["nodes"]) == 1
+ node_id = result["nodes"][0]["sharedId"]
+
+ result_in_sandbox = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" },
+ sandbox="sandbox"
+ )
+ assert len(result_in_sandbox["nodes"]) == 1
+ assert result_in_sandbox["nodes"][0]["sharedId"] == node_id
+
+
+@pytest.mark.asyncio
+async def test_locate_same_node_in_different_sandboxes_with_root_ownership_returns_different_handles(bidi_session, inline, top_context):
+ url = inline("""<div data-class="one">foobarBARbaz</div><div data-class="two">foobarBARbaz</div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ first_result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" },
+ ownership=OwnershipModel.ROOT.value,
+ sandbox="first_sandbox"
+ )
+
+ assert len(first_result["nodes"]) == 1
+
+ second_result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "div[data-class='one']" },
+ ownership=OwnershipModel.ROOT.value,
+ sandbox="second_sandbox"
+ )
+
+ assert len(second_result["nodes"]) == 1
+ assert first_result["nodes"][0]["sharedId"] == second_result["nodes"][0]["sharedId"]
+ assert first_result["nodes"][0]["handle"] != second_result["nodes"][0]["handle"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/serialization_options.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/serialization_options.py
new file mode 100644
index 0000000000..9d7e7a8613
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/serialization_options.py
@@ -0,0 +1,65 @@
+import pytest
+
+from webdriver.bidi.modules.script import SerializationOptions
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.parametrize("mode", [
+ "open",
+ "closed"
+])
+@pytest.mark.asyncio
+async def test_locate_nodes_serialization_options(bidi_session, top_context, get_test_page, mode):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(shadow_root_mode=mode),
+ wait="complete",
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": "css", "value": "custom-element" },
+ serialization_options=SerializationOptions(include_shadow_tree="all", max_dom_depth=1)
+ )
+
+ expected = [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {
+ "id": "custom-element",
+ },
+ "childNodeCount": 0,
+ "localName": "custom-element",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {
+ "id": "in-shadow-dom"
+ },
+ "childNodeCount": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }
+ ],
+ "mode": mode,
+ "nodeType": 11,
+ }
+ },
+ }
+ }
+ ]
+
+ recursive_compare(expected, result["nodes"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py
new file mode 100644
index 0000000000..707d83a337
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/locate_nodes/start_nodes.py
@@ -0,0 +1,179 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.parametrize("type,value,expected", [
+ ("css", "p", [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]),
+ ("css", "a span", [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id":"text"},
+ "childNodeCount": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]),
+ ("css", "#text", [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id":"text"},
+ "childNodeCount": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]),
+ ("xpath", "//p", [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }]),
+ ("innerText", "foo", [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"two"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }])
+])
+@pytest.mark.asyncio
+async def test_locate_with_context_nodes(bidi_session, inline, top_context, type, value, expected):
+ url = inline("""<div id="parent">
+ <p data-class="one">foo</p>
+ <p data-class="two">foo</p>
+ <a data-class="three">
+ <span id="text">bar</span>
+ </a>
+ </div>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ context_nodes = await bidi_session.script.evaluate(
+ expression="""document.querySelector("div")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": type, "value": value },
+ start_nodes=[context_nodes]
+ )
+
+ recursive_compare(expected, result["nodes"])
+
+
+@pytest.mark.parametrize("type,value", [
+ ("css", "p[data-class='one']"),
+ ("xpath", ".//p[@data-class='one']"),
+ ("innerText", "foo")
+])
+@pytest.mark.asyncio
+async def test_locate_with_multiple_context_nodes(bidi_session, inline, top_context, type, value):
+ url = inline("""
+ <div id="parent-one"><p data-class="one">foo</p><p data-class="two">bar</p></div>
+ <div id="parent-two"><p data-class="one">foo</p><p data-class="two">bar</p></div>
+ """)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ script_result = await bidi_session.script.evaluate(
+ expression="""document.querySelectorAll("div")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+
+ context_nodes = script_result["value"]
+
+ result = await bidi_session.browsing_context.locate_nodes(
+ context=top_context["context"],
+ locator={ "type": type, "value": value },
+ start_nodes=context_nodes
+ )
+
+ expected = [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"data-class":"one"},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ }
+ }
+ ]
+
+ recursive_compare(expected, result["nodes"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/__init__.py
new file mode 100644
index 0000000000..c2bf7558f4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/__init__.py
@@ -0,0 +1,28 @@
+import pytest
+
+from webdriver.bidi.error import UnknownErrorException
+
+from ... import any_string
+
+
+async def navigate_and_assert(bidi_session, context, url, wait="complete", expected_error=False):
+ if expected_error:
+ with pytest.raises(UnknownErrorException):
+ await bidi_session.browsing_context.navigate(
+ context=context['context'], url=url, wait=wait
+ )
+
+ else:
+ result = await bidi_session.browsing_context.navigate(
+ context=context['context'], url=url, wait=wait
+ )
+ assert result["url"] == url
+ any_string(result["navigation"])
+
+ contexts = await bidi_session.browsing_context.get_tree(
+ root=context['context']
+ )
+ assert len(contexts) == 1
+ assert contexts[0]["url"] == url
+
+ return contexts
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/about_blank.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/about_blank.py
new file mode 100644
index 0000000000..55ca351297
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/about_blank.py
@@ -0,0 +1,33 @@
+import pytest
+
+from . import navigate_and_assert
+
+pytestmark = pytest.mark.asyncio
+
+PAGE_ABOUT_BLANK = "about:blank"
+PAGE_EMPTY = "/webdriver/tests/bidi/browsing_context/support/empty.html"
+
+
+async def test_navigate_from_single_page(bidi_session, new_tab, url):
+ await navigate_and_assert(bidi_session, new_tab, url(PAGE_EMPTY))
+ await navigate_and_assert(bidi_session, new_tab, PAGE_ABOUT_BLANK)
+
+
+async def test_navigate_from_frameset(bidi_session, inline, new_tab, url):
+ frame_url = url(PAGE_EMPTY)
+ url_before = inline(f"<frameset><frame src='{frame_url}'/></frameset")
+ await navigate_and_assert(bidi_session, new_tab, url_before)
+
+ await navigate_and_assert(bidi_session, new_tab, PAGE_ABOUT_BLANK)
+
+
+async def test_navigate_in_iframe(bidi_session, inline, new_tab):
+ frame_start_url = inline("frame")
+ url_before = inline(f"<iframe src='{frame_start_url}'></iframe>")
+ contexts = await navigate_and_assert(bidi_session, new_tab, url_before)
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+ assert frame["url"] == frame_start_url
+
+ await navigate_and_assert(bidi_session, frame, PAGE_ABOUT_BLANK)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/data_url.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/data_url.py
new file mode 100644
index 0000000000..390b519034
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/data_url.py
@@ -0,0 +1,101 @@
+from urllib.parse import quote
+
+import pytest
+
+from . import navigate_and_assert
+
+pytestmark = pytest.mark.asyncio
+
+
+def dataURL(doc, mime_type="text/html", charset="utf-8", is_base64=False):
+ encoding = ""
+ if charset:
+ encoding = f"charset={charset}"
+ elif is_base64:
+ encoding = "base64"
+
+ return f"data:{mime_type};{encoding},{quote(doc)}"
+
+
+HTML_BAR = dataURL("<p>bar</p>")
+HTML_FOO = dataURL("<p>foo</p>")
+IMG_BLACK_PIXEL = dataURL(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
+ "image/png",
+ None,
+ True,
+)
+IMG_RED_PIXEL = dataURL(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=",
+ "image/png",
+ None,
+ True,
+)
+PAGE = "/webdriver/tests/bidi/browsing_context/support/empty.html"
+TEXT_BAR = dataURL("bar", "text/plain")
+TEXT_FOO = dataURL("foo", "text/plain")
+
+
+def wrap_content_in_url(url, content):
+ """Check if content is not data url and wrap it in the url function"""
+ if content.startswith("data:"):
+ return content
+ return url(content)
+
+
+@pytest.mark.parametrize(
+ "url_before, url_after",
+ [
+ (PAGE, IMG_BLACK_PIXEL),
+ (IMG_BLACK_PIXEL, IMG_RED_PIXEL),
+ (IMG_BLACK_PIXEL, HTML_FOO),
+ (IMG_BLACK_PIXEL, PAGE),
+ (PAGE, HTML_FOO),
+ (HTML_FOO, TEXT_FOO),
+ (HTML_FOO, HTML_BAR),
+ (HTML_FOO, PAGE),
+ (PAGE, TEXT_FOO),
+ (TEXT_FOO, TEXT_BAR),
+ (TEXT_FOO, IMG_BLACK_PIXEL),
+ (TEXT_FOO, PAGE),
+ ],
+ ids=[
+ "document to data:image",
+ "data:image to data:image",
+ "data:image to data:html",
+ "data:image to document",
+ "document to data:html",
+ "data:html to data:html",
+ "data:html to data:text",
+ "data:html to document",
+ "document to data:text",
+ "data:text to data:text",
+ "data:text to data:image",
+ "data:text to document",
+ ],
+)
+async def test_navigate_from_single_page(
+ bidi_session, new_tab, url, url_before, url_after
+):
+ await navigate_and_assert(
+ bidi_session,
+ new_tab,
+ wrap_content_in_url(url, url_before),
+ )
+ await navigate_and_assert(
+ bidi_session,
+ new_tab,
+ wrap_content_in_url(url, url_after),
+ )
+
+
+async def test_navigate_in_iframe(bidi_session, inline, new_tab):
+ frame_start_url = inline("frame")
+ url_before = inline(f"<iframe src='{frame_start_url}'></iframe>")
+ contexts = await navigate_and_assert(bidi_session, new_tab, url_before)
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+ assert frame["url"] == frame_start_url
+
+ await navigate_and_assert(bidi_session, frame, HTML_BAR)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/error.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/error.py
new file mode 100644
index 0000000000..ba23e77300
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/error.py
@@ -0,0 +1,22 @@
+import pytest
+
+from . import navigate_and_assert
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "url",
+ [
+ "thisprotocoldoesnotexist://",
+ "https://doesnotexist.localhost/",
+ "https://localhost:0",
+ ],
+ ids=[
+ "protocol",
+ "host",
+ "port",
+ ]
+)
+async def test_invalid_address(bidi_session, new_tab, url):
+ await navigate_and_assert(bidi_session, new_tab, url, expected_error=True)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/frame.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/frame.py
new file mode 100644
index 0000000000..4dcd88dfdb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/frame.py
@@ -0,0 +1,59 @@
+import pytest
+
+from . import navigate_and_assert
+
+pytestmark = pytest.mark.asyncio
+
+PAGE_CONTENT = "<div>foo</div>"
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_origin(bidi_session, new_tab, inline, domain):
+ frame_start_url = inline("frame")
+ url_before = inline(f"<iframe src='{frame_start_url}'></iframe>", domain=domain)
+ contexts = await navigate_and_assert(bidi_session, new_tab, url_before)
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+ assert frame["url"] == frame_start_url
+
+ await navigate_and_assert(bidi_session, frame, inline(PAGE_CONTENT))
+
+
+async def test_multiple_frames(
+ bidi_session, new_tab, test_page_multiple_frames, test_page, test_page2, inline
+):
+ contexts = await navigate_and_assert(
+ bidi_session, new_tab, test_page_multiple_frames
+ )
+
+ assert len(contexts[0]["children"]) == 2
+ frame = contexts[0]["children"][0]
+ assert frame["url"] == test_page
+
+ await navigate_and_assert(bidi_session, frame, inline(PAGE_CONTENT))
+
+ # Make sure that the second frame hasn't been navigated
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ assert contexts[0]["children"][1]["url"] == test_page2
+
+
+async def test_nested_frames(
+ bidi_session,
+ new_tab,
+ inline,
+ test_page_nested_frames,
+ test_page_same_origin_frame,
+ test_page,
+):
+ contexts = await navigate_and_assert(bidi_session, new_tab, test_page_nested_frames)
+
+ assert len(contexts[0]["children"]) == 1
+ frame_level_1 = contexts[0]["children"][0]
+ assert frame_level_1["url"] == test_page_same_origin_frame
+
+ assert len(frame_level_1["children"]) == 1
+ frame_level_2 = frame_level_1["children"][0]
+ assert frame_level_2["url"] == test_page
+
+ await navigate_and_assert(bidi_session, frame_level_2, inline(PAGE_CONTENT))
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/hash.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/hash.py
new file mode 100644
index 0000000000..7dc520c4e3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/hash.py
@@ -0,0 +1,90 @@
+import pytest
+
+from . import navigate_and_assert
+from ... import any_string
+
+pytestmark = pytest.mark.asyncio
+
+PAGE_EMPTY = "/webdriver/tests/bidi/browsing_context/support/empty.html"
+PAGE_EMPTY_WITH_HASH_FOO = f"{PAGE_EMPTY}#foo"
+PAGE_OTHER = "/webdriver/tests/bidi/browsing_context/support/other.html"
+
+
+@pytest.mark.parametrize(
+ "hash_before, hash_after",
+ [
+ ("", "#foo"),
+ ("#foo", "#bar"),
+ ("#foo", "#foo"),
+ ("#bar", ""),
+ ],
+ ids=[
+ "without hash to with hash",
+ "with different hashes",
+ "with identical hashes",
+ "with hash to without hash",
+ ],
+)
+async def test_navigate_in_the_same_document(
+ bidi_session, new_tab, url, hash_before, hash_after
+):
+ await navigate_and_assert(bidi_session, new_tab, url(PAGE_EMPTY + hash_before))
+ await navigate_and_assert(bidi_session, new_tab, url(PAGE_EMPTY + hash_after))
+
+
+@pytest.mark.parametrize(
+ "url_before, url_after",
+ [
+ (PAGE_EMPTY_WITH_HASH_FOO, f"{PAGE_OTHER}#foo"),
+ (PAGE_EMPTY_WITH_HASH_FOO, f"{PAGE_OTHER}#bar"),
+ ],
+ ids=[
+ "with identical hashes",
+ "with different hashes",
+ ],
+)
+async def test_navigate_different_documents(
+ bidi_session, new_tab, url, url_before, url_after
+):
+ await navigate_and_assert(bidi_session, new_tab, url(url_before))
+ await navigate_and_assert(bidi_session, new_tab, url(url_after))
+
+
+async def test_navigate_in_iframe(bidi_session, inline, new_tab):
+ frame_start_url = inline("frame")
+ url_before = inline(f"<iframe src='{frame_start_url}'></iframe>")
+ contexts = await navigate_and_assert(bidi_session, new_tab, url_before)
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+ assert frame["url"] == frame_start_url
+
+ url_after = f"{frame_start_url}#foo"
+ await navigate_and_assert(bidi_session, frame, url_after)
+
+
+async def test_navigate_unique_navigation_id(bidi_session, inline, new_tab):
+ url = inline("<div>foo</div>")
+
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+ any_string(result["navigation"])
+
+ hash_result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=f"{url}#foo", wait="complete"
+ )
+ any_string(hash_result["navigation"])
+ assert hash_result["navigation"] != result["navigation"]
+
+ other_hash_result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=f"{url}#bar", wait="complete"
+ )
+ any_string(other_hash_result["navigation"])
+ assert other_hash_result["navigation"] != hash_result["navigation"]
+
+ same_hash_result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=f"{url}#bar", wait="complete"
+ )
+ any_string(same_hash_result["navigation"])
+ assert same_hash_result["navigation"] != other_hash_result["navigation"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/image.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/image.py
new file mode 100644
index 0000000000..79030c9fe1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/image.py
@@ -0,0 +1,56 @@
+import pytest
+
+from . import navigate_and_assert
+
+pytestmark = pytest.mark.asyncio
+
+PAGE_EMPTY = "/webdriver/tests/bidi/browsing_context/support/empty.html"
+PNG_BLACK_DOT = "/webdriver/tests/bidi/browsing_context/support/black_dot.png"
+PNG_RED_DOT = "/webdriver/tests/bidi/browsing_context/support/red_dot.png"
+SVG = "/webdriver/tests/bidi/browsing_context/support/other.svg"
+
+
+@pytest.mark.parametrize(
+ "url_before, url_after",
+ [
+ (PAGE_EMPTY, SVG),
+ (SVG, PAGE_EMPTY),
+ (PAGE_EMPTY, PNG_BLACK_DOT),
+ (PNG_BLACK_DOT, PNG_RED_DOT),
+ (PNG_RED_DOT, SVG),
+ (PNG_BLACK_DOT, PAGE_EMPTY),
+ ],
+ ids=[
+ "document to svg",
+ "svg to document",
+ "document to png",
+ "png to png",
+ "png to svg",
+ "png to document",
+ ],
+)
+async def test_navigate_between_img_and_html(
+ bidi_session, new_tab, url, url_before, url_after
+):
+ await navigate_and_assert(bidi_session, new_tab, url(url_before))
+ await navigate_and_assert(bidi_session, new_tab, url(url_after))
+
+
+@pytest.mark.parametrize(
+ "img",
+ [SVG, PNG_BLACK_DOT],
+ ids=[
+ "to svg",
+ "to png",
+ ],
+)
+async def test_navigate_in_iframe(bidi_session, new_tab, inline, url, img):
+ frame_start_url = inline("frame")
+ url_before = inline(f"<iframe src='{frame_start_url}'></iframe>")
+ contexts = await navigate_and_assert(bidi_session, new_tab, url_before)
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+ assert frame["url"] == frame_start_url
+
+ await navigate_and_assert(bidi_session, frame, url(img))
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/invalid.py
new file mode 100644
index 0000000000..1f33604c4d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/invalid.py
@@ -0,0 +1,53 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, inline, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.navigate(
+ context=value, url=inline("<p>foo")
+ )
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_params_context_invalid_value(bidi_session, inline, value):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.navigate(
+ context=value, url=inline("<p>foo")
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_url_invalid_type(bidi_session, new_tab, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=value
+ )
+
+
+@pytest.mark.parametrize("protocol", ["http", "https"])
+@pytest.mark.parametrize("value", [":invalid", "#invalid"])
+async def test_params_url_invalid_value(bidi_session, new_tab, protocol, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=f"{protocol}://{value}"
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_wait_invalid_type(bidi_session, inline, new_tab, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<p>bar"), wait=value
+ )
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_params_wait_invalid_value(bidi_session, inline, new_tab, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<p>bar"), wait=value
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/navigate.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/navigate.py
new file mode 100644
index 0000000000..934fd3554f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/navigate.py
@@ -0,0 +1,88 @@
+import asyncio
+
+import pytest
+
+from . import navigate_and_assert
+from ... import any_string
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_payload(bidi_session, inline, new_tab):
+ url = inline("<div>foo</div>")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+
+ any_string(result["navigation"])
+ assert result["url"] == url
+
+
+async def test_interactive_simultaneous_navigation(bidi_session, wait_for_future_safe, inline, new_tab):
+ frame1_start_url = inline("frame1")
+ frame2_start_url = inline("frame2")
+
+ url = inline(
+ f"<iframe src='{frame1_start_url}'></iframe><iframe src='{frame2_start_url}'></iframe>"
+ )
+
+ contexts = await navigate_and_assert(bidi_session, new_tab, url)
+ assert len(contexts[0]["children"]) == 2
+
+ frame1_context_id = contexts[0]["children"][0]["context"]
+ frame2_context_id = contexts[0]["children"][1]["context"]
+
+ # The goal here is to navigate both iframes in parallel, and to use the
+ # interactive wait condition for both.
+ # Make sure that monitoring the DOMContentLoaded event for one frame does
+ # prevent monitoring it for the other frame.
+ img_url = "/webdriver/tests/bidi/browsing_context/support/empty.svg"
+ script_url = "/webdriver/tests/bidi/browsing_context/support/empty.js"
+ # frame1 also has a slow loading image so that it won't reach a complete
+ # navigation, and we can make sure we resolved with the interactive state.
+ frame1_url = inline(
+ f"""frame1_new<script src='{script_url}?pipe=trickle(d2)'></script>
+ <img src='{img_url}?pipe=trickle(d100)'>
+ """
+ )
+ frame2_url = inline(
+ f"frame2_new<script src='{script_url}?pipe=trickle(d0.5)'></script>"
+ )
+
+ frame1_task = asyncio.ensure_future(
+ bidi_session.browsing_context.navigate(
+ context=frame1_context_id, url=frame1_url, wait="interactive"
+ )
+ )
+
+ frame2_result = await bidi_session.browsing_context.navigate(
+ context=frame2_context_id, url=frame2_url, wait="interactive"
+ )
+ assert frame2_result["url"] == frame2_url
+
+ # The "interactive" navigation should resolve before the 5 seconds timeout.
+ await wait_for_future_safe(frame1_task, timeout=5)
+
+ frame1_result = frame1_task.result()
+ assert frame1_result["url"] == frame1_url
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ assert contexts[0]["children"][0]["url"] == frame1_url
+ assert contexts[0]["children"][1]["url"] == frame2_url
+
+ any_string(frame1_result["navigation"])
+ any_string(frame2_result["navigation"])
+ assert frame1_result["navigation"] != frame2_result["navigation"]
+
+
+async def test_relative_url(bidi_session, new_tab, url):
+ url_before = url(
+ "/webdriver/tests/bidi/browsing_context/support/empty.html"
+ )
+
+ # Navigate to page1 with wait=interactive to make sure the document's base URI
+ # was updated.
+ await navigate_and_assert(bidi_session, new_tab, url_before, "interactive")
+
+ url_after = url_before.replace("empty.html", "other.html")
+ await navigate_and_assert(bidi_session, new_tab, url_after, "interactive")
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/wait.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/wait.py
new file mode 100644
index 0000000000..3a351e1089
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigate/wait.py
@@ -0,0 +1,98 @@
+import pytest
+import asyncio
+
+pytestmark = pytest.mark.asyncio
+
+
+async def wait_for_navigation(bidi_session, context, url, wait, expect_timeout):
+ # Ultimately, "interactive" and "complete" should support a timeout argument.
+ # See https://github.com/w3c/webdriver-bidi/issues/188.
+ if expect_timeout:
+ with pytest.raises(asyncio.TimeoutError):
+ await asyncio.wait_for(
+ asyncio.shield(bidi_session.browsing_context.navigate(
+ context=context, url=url, wait=wait
+ )),
+ timeout=1,
+ )
+ else:
+ await bidi_session.browsing_context.navigate(
+ context=context, url=url, wait=wait
+ )
+
+
+@pytest.mark.parametrize("value", ["none", "interactive", "complete"])
+async def test_expected_url(bidi_session, inline, new_tab, value):
+ url = inline("<div>foo</div>")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait=value
+ )
+ assert result["url"] == url
+ if value != "none":
+ contexts = await bidi_session.browsing_context.get_tree(
+ root=new_tab["context"], max_depth=0
+ )
+ assert contexts[0]["url"] == url
+
+
+@pytest.mark.parametrize(
+ "wait, expect_timeout",
+ [
+ ("none", False),
+ ("interactive", False),
+ ("complete", True),
+ ],
+)
+async def test_slow_image_blocks_load(bidi_session, inline, new_tab, wait, expect_timeout):
+ image_url = "/webdriver/tests/bidi/browsing_context/support/empty.svg"
+ url = inline(f"<img src='{image_url}?pipe=trickle(d10)'>")
+
+ await wait_for_navigation(bidi_session, new_tab["context"], url, wait, expect_timeout)
+
+ # We cannot assert the URL for "none" by definition, and for "complete", since
+ # we expect a timeout. For the timeout case, the wait_for_navigation helper will
+ # resume after 1 second, there is no guarantee that the URL has been updated.
+ if wait == "interactive":
+ contexts = await bidi_session.browsing_context.get_tree(
+ root=new_tab["context"], max_depth=0
+ )
+ assert contexts[0]["url"] == url
+
+
+@pytest.mark.parametrize(
+ "wait, expect_timeout",
+ [
+ ("none", False),
+ ("interactive", True),
+ ("complete", True),
+ ],
+)
+async def test_slow_page(bidi_session, new_tab, url, wait, expect_timeout):
+ page_url = url(
+ "/webdriver/tests/bidi/browsing_context/support/empty.html?pipe=trickle(d10)"
+ )
+
+ await wait_for_navigation(bidi_session, new_tab["context"], page_url, wait, expect_timeout)
+
+ # Note that we cannot assert the top context url here, because the navigation
+ # is blocked on the initial url for this test case.
+
+
+@pytest.mark.parametrize(
+ "wait, expect_timeout",
+ [
+ ("none", False),
+ ("interactive", True),
+ ("complete", True),
+ ],
+)
+async def test_slow_script_blocks_domContentLoaded(bidi_session, inline, new_tab, wait, expect_timeout):
+ script_url = "/webdriver/tests/bidi/browsing_context/support/empty.js"
+ url = inline(f"<script src='{script_url}?pipe=trickle(d10)'></script>")
+
+ await wait_for_navigation(bidi_session, new_tab["context"], url, wait, expect_timeout)
+
+ # In theory we could also assert the top context URL has been updated here
+ # but since we expect both "interactive" and "complete" to timeout, the
+ # wait_for_navigation helper will resume arbitrarily after 1 second, and
+ # there is no guarantee that the URL has been updated.
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/navigation_started.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/navigation_started.py
new file mode 100644
index 0000000000..af91f0110a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/navigation_started/navigation_started.py
@@ -0,0 +1,463 @@
+import pytest
+from tests.support.sync import AsyncPoll
+
+from webdriver.error import TimeoutException
+from webdriver.bidi.error import UnknownErrorException
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import int_interval
+from .. import assert_navigation_info
+
+
+pytestmark = pytest.mark.asyncio
+
+NAVIGATION_STARTED_EVENT = "browsingContext.navigationStarted"
+PAGE_EMPTY = "/webdriver/tests/bidi/browsing_context/support/empty.html"
+PAGE_REDIRECT_HTTP_EQUIV = (
+ "/webdriver/tests/bidi/network/support/redirect_http_equiv.html"
+)
+PAGE_REDIRECTED_HTML = "/webdriver/tests/bidi/network/support/redirected.html"
+
+
+async def test_unsubscribe(bidi_session):
+ await bidi_session.session.subscribe(events=[NAVIGATION_STARTED_EVENT])
+ await bidi_session.session.unsubscribe(events=[NAVIGATION_STARTED_EVENT])
+
+ # Track all received browsingContext.navigationStarted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ NAVIGATION_STARTED_EVENT, on_event
+ )
+
+ await bidi_session.browsing_context.create(type_hint="tab")
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+async def test_subscribe(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ on_entry = wait_for_event(NAVIGATION_STARTED_EVENT)
+ url = inline("<div>foo</div>")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event,
+ {
+ "context": new_tab["context"],
+ "navigation": result["navigation"],
+ "url": url,
+ },
+ )
+
+
+async def test_timestamp(
+ bidi_session, current_time, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ time_start = await current_time()
+
+ on_entry = wait_for_event(NAVIGATION_STARTED_EVENT)
+ url = inline("<div>foo</div>")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ time_end = await current_time()
+
+ assert_navigation_info(
+ event,
+ {
+ "context": new_tab["context"],
+ "navigation": result["navigation"],
+ "timestamp": int_interval(time_start, time_end),
+ },
+ )
+
+
+async def test_iframe(
+ bidi_session, subscribe_events, top_context, test_page_same_origin_frame, test_page
+):
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ NAVIGATION_STARTED_EVENT, on_event
+ )
+
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_same_origin_frame, wait="complete"
+ )
+
+ # Check that 2 navigation-started events were received, one for the top context
+ # and one for the iframe.
+ assert len(events) == 2
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ children_info = root_info["children"]
+ assert len(children_info) == 1
+
+ # First navigation-started event comes from the top-level browsing context.
+ assert_navigation_info(
+ events[0],
+ {
+ "context": top_context["context"],
+ "navigation": result["navigation"],
+ "url": test_page_same_origin_frame,
+ },
+ )
+
+ assert_navigation_info(
+ events[1],
+ {
+ "context": children_info[0]["context"],
+ "url": test_page,
+ },
+ )
+ assert events[1]["navigation"] is not None
+ assert events[1]["navigation"] != result["navigation"]
+
+ remove_listener()
+
+
+async def test_nested_iframes(
+ bidi_session,
+ subscribe_events,
+ top_context,
+ test_page_nested_frames,
+ test_page_same_origin_frame,
+ test_page,
+):
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ NAVIGATION_STARTED_EVENT, on_event
+ )
+
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_nested_frames, wait="complete"
+ )
+
+ # Check that 3 navigation-started events were received, one for the top context
+ # and one for each of the 2 iframes.
+ assert len(events) == 3
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts) == 1
+ root_info = contexts[0]
+ assert len(root_info["children"]) == 1
+ child1_info = root_info["children"][0]
+ assert len(child1_info["children"]) == 1
+ child2_info = child1_info["children"][0]
+
+ assert_navigation_info(
+ events[0],
+ {
+ "context": root_info["context"],
+ "navigation": result["navigation"],
+ "url": test_page_nested_frames,
+ },
+ )
+
+ assert_navigation_info(
+ events[1],
+ {
+ "context": child1_info["context"],
+ "url": test_page_same_origin_frame,
+ },
+ )
+ assert events[1]["navigation"] is not None
+ assert events[1]["navigation"] != result["navigation"]
+
+ assert_navigation_info(
+ events[2],
+ {
+ "context": child2_info["context"],
+ "url": test_page,
+ },
+ )
+ assert events[2]["navigation"] is not None
+ assert events[2]["navigation"] != result["navigation"]
+ assert events[2]["navigation"] != events[1]["navigation"]
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_new_context(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, type_hint):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ on_entry = wait_for_event(NAVIGATION_STARTED_EVENT)
+ top_level_context = await bidi_session.browsing_context.create(type_hint="tab")
+ navigation_info = await wait_for_future_safe(on_entry)
+ assert_navigation_info(
+ navigation_info,
+ {
+ "context": top_level_context["context"],
+ "url": "about:blank",
+ },
+ )
+
+
+async def test_same_document_navigation(bidi_session, new_tab, url, subscribe_events):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(PAGE_EMPTY), wait="complete"
+ )
+
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ # Track all received browsingContext.navigationStarted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ NAVIGATION_STARTED_EVENT, on_event
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url(PAGE_EMPTY + "#foo"), wait="complete"
+ )
+
+ remove_listener()
+
+
+async def test_window_open(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, top_context):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ on_entry = wait_for_event(NAVIGATION_STARTED_EVENT)
+
+ await bidi_session.script.evaluate(
+ expression="""window.open('about:blank');""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ navigation_info = await wait_for_future_safe(on_entry)
+ assert_navigation_info(
+ navigation_info,
+ {
+ "url": "about:blank",
+ },
+ )
+ assert navigation_info["navigation"] is not None
+
+ # Retrieve all contexts to get the context for the new window.
+ contexts = await bidi_session.browsing_context.get_tree()
+ assert navigation_info["context"] == contexts[-1]["context"]
+
+
+async def test_document_write(bidi_session, subscribe_events, top_context):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ # Track all received browsingContext.navigationStarted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ NAVIGATION_STARTED_EVENT, on_event
+ )
+
+ await bidi_session.script.evaluate(
+ expression="""document.open(); document.write("<h1>Replaced</h1>"); document.close();""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+async def test_page_with_base_tag(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ on_entry = wait_for_event(NAVIGATION_STARTED_EVENT)
+ url = inline("""<base href="/relative-path">""")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ assert_navigation_info(
+ event,
+ {"context": new_tab["context"], "navigation": result["navigation"], "url": url},
+ )
+
+
+@pytest.mark.parametrize(
+ "url",
+ [
+ "thisprotocoldoesnotexist://",
+ "https://doesnotexist.localhost/",
+ ],
+ ids=[
+ "protocol",
+ "host",
+ ],
+)
+async def test_invalid_navigation(
+ bidi_session, new_tab, subscribe_events, wait_for_event, wait_for_future_safe, url
+):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ on_entry = wait_for_event(NAVIGATION_STARTED_EVENT)
+
+ with pytest.raises(UnknownErrorException):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ navigation_info = await wait_for_future_safe(on_entry)
+ assert_navigation_info(
+ navigation_info,
+ {
+ "context": new_tab["context"],
+ "url": url,
+ },
+ )
+ assert navigation_info["navigation"] is not None
+
+ await bidi_session.session.unsubscribe(events=[NAVIGATION_STARTED_EVENT])
+
+
+async def test_redirect_http_equiv(
+ bidi_session, subscribe_events, top_context, url
+):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ # Track all received browsingContext.navigationStarted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ NAVIGATION_STARTED_EVENT, on_event
+ )
+
+ # PAGE_REDIRECT_HTTP_EQUIV should redirect to PAGE_REDIRECTED_HTML immediately
+ http_equiv_url = url(PAGE_REDIRECT_HTTP_EQUIV)
+ redirected_url = url(PAGE_REDIRECTED_HTML)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=http_equiv_url,
+ wait="complete",
+ )
+
+ # Wait until we receive two events, one for the initial navigation and one
+ # for the http-equiv "redirect".
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+
+ assert len(events) == 2
+ assert_navigation_info(
+ events[0],
+ {
+ "context": top_context["context"],
+ "url": http_equiv_url,
+ },
+ )
+ assert_navigation_info(
+ events[1],
+ {
+ "context": top_context["context"],
+ "url": redirected_url,
+ },
+ )
+
+ remove_listener()
+
+
+async def test_redirect_navigation(
+ bidi_session, subscribe_events, top_context, url
+):
+ await subscribe_events(events=[NAVIGATION_STARTED_EVENT])
+
+ # Track all received browsingContext.navigationStarted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ NAVIGATION_STARTED_EVENT, on_event
+ )
+
+ html_url = url(PAGE_EMPTY)
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={html_url}"
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=redirect_url,
+ wait="complete",
+ )
+
+ assert len(events) == 1
+ assert_navigation_info(
+ events[0],
+ {
+ "context": top_context["context"],
+ "url": redirect_url,
+ },
+ )
+
+ remove_listener()
+
+
+async def test_navigate_history_pushstate(
+ bidi_session, inline, new_tab, subscribe_events, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events([NAVIGATION_STARTED_EVENT])
+
+ on_entry = wait_for_event(NAVIGATION_STARTED_EVENT)
+ url = inline("""
+ <script>
+ window.addEventListener('DOMContentLoaded', () => {
+ history.pushState({}, '', '#1');
+ });
+ </script>""")
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+ event = await wait_for_future_safe(on_entry)
+
+ assert event["navigation"] == result["navigation"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/background.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/background.py
new file mode 100644
index 0000000000..5f1f518928
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/background.py
@@ -0,0 +1,58 @@
+import base64
+import pytest
+
+from tests.support.asserts import assert_pdf
+from tests.support.image import pt_to_cm
+
+pytestmark = pytest.mark.asyncio
+
+INLINE_BACKGROUND_RENDERING_TEST_CONTENT = """
+<style>
+:root {
+ background-color: black;
+}
+</style>
+"""
+
+BLACK_DOT_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2OUAAAAABJRU5ErkJggg=="
+WHITE_DOT_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2P4DwQACfsD/Z8fLAAAAAAASUVORK5CYII="
+
+
+@pytest.mark.parametrize("print_with_background, expected_image", [
+ (None, WHITE_DOT_PNG),
+ (True, BLACK_DOT_PNG),
+ (False, WHITE_DOT_PNG),
+], ids=["default", "true", "false"])
+async def test_background(
+ bidi_session,
+ top_context,
+ inline,
+ compare_png_bidi,
+ render_pdf_to_png_bidi,
+ print_with_background,
+ expected_image,
+):
+ page = inline(INLINE_BACKGROUND_RENDERING_TEST_CONTENT)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=page, wait="complete")
+
+ print_value = await bidi_session.browsing_context.print(
+ context=top_context["context"],
+ background=print_with_background,
+ margin={
+ "top": 0,
+ "bottom": 0,
+ "right": 0,
+ "left": 0
+ },
+ page={
+ "width": pt_to_cm(1),
+ "height": pt_to_cm(1),
+ },
+ )
+
+ assert_pdf(print_value)
+
+ png = await render_pdf_to_png_bidi(print_value)
+ comparison = await compare_png_bidi(png, base64.b64decode(expected_image))
+ assert comparison.equal()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/context.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/context.py
new file mode 100644
index 0000000000..f8074b71b4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/context.py
@@ -0,0 +1,61 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_context(bidi_session, top_context, inline, assert_pdf_content):
+ text = "Test"
+ url = inline(text)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ value = await bidi_session.browsing_context.print(context=top_context["context"])
+
+ await assert_pdf_content(value, [{"type": "string", "value": text}])
+
+
+async def test_page_with_iframe(
+ bidi_session, top_context, inline, iframe, assert_pdf_content
+):
+ text = "Test"
+ iframe_content = "Iframe"
+ url = inline(f"{text}<br/>{iframe(iframe_content)}")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ whole_page_value = await bidi_session.browsing_context.print(
+ context=top_context["context"]
+ )
+
+ await assert_pdf_content(
+ whole_page_value, [{"type": "string", "value": text + iframe_content}]
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context = contexts[0]["children"][0]
+
+ frame_value = await bidi_session.browsing_context.print(
+ context=frame_context["context"]
+ )
+
+ await assert_pdf_content(frame_value, [{"type": "string", "value": iframe_content}])
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_context_origin(
+ bidi_session, top_context, inline, iframe, assert_pdf_content, domain
+):
+ iframe_content = "Iframe"
+ url = inline(f"{iframe(iframe_content, domain=domain)}")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context = contexts[0]["children"][0]
+
+ value = await bidi_session.browsing_context.print(context=frame_context["context"])
+
+ await assert_pdf_content(value, [{"type": "string", "value": iframe_content}])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/invalid.py
new file mode 100644
index 0000000000..78f9a13cba
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/invalid.py
@@ -0,0 +1,200 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("context", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(context=context)
+
+
+async def test_params_context_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.print(context="_invalid_")
+
+
+async def test_params_context_closed(bidi_session):
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ # Try to print the closed context
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.print(context=new_tab["context"])
+
+
+@pytest.mark.parametrize("background", ["foo", 42, {}, []])
+async def test_params_background_invalid_type(bidi_session, top_context, background):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], background=background
+ )
+
+
+@pytest.mark.parametrize(
+ "margin",
+ [
+ False,
+ "foo",
+ 42,
+ [],
+ {"top": False},
+ {"top": "foo"},
+ {"top": []},
+ {"top": {}},
+ {"bottom": False},
+ {"bottom": "foo"},
+ {"bottom": []},
+ {"bottom": {}},
+ {"left": False},
+ {"left": "foo"},
+ {"left": []},
+ {"left": {}},
+ {"right": False},
+ {"right": "foo"},
+ {"right": []},
+ {"right": {}},
+ ],
+)
+async def test_params_margin_invalid_type(bidi_session, top_context, margin):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], margin=margin
+ )
+
+
+@pytest.mark.parametrize(
+ "margin",
+ [
+ {"top": -0.1},
+ {"bottom": -0.1},
+ {"left": -0.1},
+ {"right": -0.1},
+ ],
+)
+async def test_params_margin_invalid_value(bidi_session, top_context, margin):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], margin=margin
+ )
+
+
+@pytest.mark.parametrize("orientation", [False, 42, {}, []])
+async def test_params_orientation_invalid_type(bidi_session, top_context, orientation):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], orientation=orientation
+ )
+
+
+async def test_params_orientation_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], orientation="foo"
+ )
+
+
+@pytest.mark.parametrize(
+ "page",
+ [
+ False,
+ "foo",
+ 42,
+ [],
+ {"height": False},
+ {"height": "foo"},
+ {"height": []},
+ {"height": {}},
+ {"width": False},
+ {"width": "foo"},
+ {"width": []},
+ {"width": {}},
+ ],
+)
+async def test_params_page_invalid_type(bidi_session, top_context, page):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], page=page
+ )
+
+
+@pytest.mark.parametrize(
+ "page",
+ [
+ {"height": -1},
+ {"width": -1},
+ {"height": 0.03},
+ {"width": 0.03},
+ ],
+)
+async def test_params_page_invalid_value(bidi_session, top_context, page):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], page=page
+ )
+
+
+@pytest.mark.parametrize(
+ "page_ranges",
+ [
+ False,
+ "foo",
+ 42,
+ {},
+ [None],
+ [False],
+ [[]],
+ [{}],
+ ["1-2", {}],
+ ],
+)
+async def test_params_page_ranges_invalid_type(bidi_session, top_context, page_ranges):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], page_ranges=page_ranges
+ )
+
+
+@pytest.mark.parametrize(
+ "page_ranges",
+ [
+ [4.2],
+ ["4.2"],
+ ["3-2"],
+ ["a-2"],
+ ["1:2"],
+ ["1-2-3"],
+ ],
+)
+async def test_params_page_ranges_invalid_value(bidi_session, top_context, page_ranges):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], page_ranges=page_ranges
+ )
+
+
+@pytest.mark.parametrize("scale", [False, "foo", {}, []])
+async def test_params_scale_invalid_type(bidi_session, top_context, scale):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], scale=scale
+ )
+
+
+@pytest.mark.parametrize("scale", [-1, 0.09, 2.01, 42])
+async def test_params_scale_invalid_value(bidi_session, top_context, scale):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], scale=scale
+ )
+
+
+@pytest.mark.parametrize("shrink_to_fit", ["foo", 42, {}, []])
+async def test_params_shrink_to_fit_invalid_type(
+ bidi_session, top_context, shrink_to_fit
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"], shrink_to_fit=shrink_to_fit
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/margin.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/margin.py
new file mode 100644
index 0000000000..1863cec1ca
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/margin.py
@@ -0,0 +1,215 @@
+# META: timeout=long
+from math import ceil
+import pytest
+
+from webdriver.bidi.error import UnsupportedOperationException
+from tests.support.image import inch_in_cm, inch_in_point
+
+pytestmark = pytest.mark.asyncio
+
+DEFAULT_PAGE_HEIGHT = 27.94
+DEFAULT_PAGE_WIDTH = 21.59
+
+
+def get_content(css=""):
+ return f"""
+ <div></div>
+ <style>
+ html,
+ body {{
+ margin: 0;
+ }}
+ div {{
+ background-color: black;
+ height: {DEFAULT_PAGE_HEIGHT}cm;
+ {css}
+ }}
+ </style>
+ """
+
+
+@pytest.mark.parametrize(
+ "margin, reference_css, css",
+ [
+ (
+ {"top": inch_in_cm},
+ "margin-top: 1.54cm;",
+ "",
+ ),
+ (
+ {"left": inch_in_cm},
+ "margin-left: 1.54cm;",
+ "",
+ ),
+ (
+ {"right": inch_in_cm},
+ "margin-right: 1.54cm;",
+ "",
+ ),
+ (
+ {"bottom": inch_in_cm},
+ "height: 24.4cm;",
+ "height: 26.94cm;",
+ ),
+ ],
+ ids=[
+ "top",
+ "left",
+ "right",
+ "bottom",
+ ],
+)
+async def test_margin_default(
+ bidi_session,
+ top_context,
+ inline,
+ assert_pdf_image,
+ margin,
+ reference_css,
+ css,
+):
+ default_content_page = inline(get_content(css))
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=default_content_page,
+ wait="complete"
+ )
+ value_with_margin = await bidi_session.browsing_context.print(
+ context=top_context["context"],
+ shrink_to_fit=False,
+ background=True,
+ margin=margin,
+ )
+
+ # Compare a page with default margin (1.0cm) + css margin
+ # with a page with extended print margin.
+ await assert_pdf_image(value_with_margin, get_content(reference_css), True)
+
+
+@pytest.mark.parametrize(
+ "margin",
+ [
+ {"top": DEFAULT_PAGE_HEIGHT},
+ {"left": DEFAULT_PAGE_WIDTH},
+ {"right": DEFAULT_PAGE_WIDTH},
+ {"bottom": DEFAULT_PAGE_HEIGHT},
+ {
+ "top": DEFAULT_PAGE_HEIGHT,
+ "left": DEFAULT_PAGE_WIDTH,
+ "right": DEFAULT_PAGE_WIDTH,
+ "bottom": DEFAULT_PAGE_HEIGHT,
+ },
+ ],
+ ids=[
+ "top",
+ "left",
+ "right",
+ "bottom",
+ "all",
+ ],
+)
+async def test_margin_same_as_page_dimension(
+ bidi_session,
+ top_context,
+ inline,
+ margin,
+):
+ page = inline("Text")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=page, wait="complete"
+ )
+
+ # This yields an empty content area: https://github.com/w3c/webdriver-bidi/issues/473
+ with pytest.raises(UnsupportedOperationException):
+ await bidi_session.browsing_context.print(
+ context=top_context["context"],
+ shrink_to_fit=False,
+ margin=margin,
+ )
+
+
+@pytest.mark.parametrize(
+ "margin",
+ [
+ {"top": DEFAULT_PAGE_HEIGHT - ceil(inch_in_cm / inch_in_point)},
+ {"left": DEFAULT_PAGE_WIDTH - ceil(inch_in_cm / inch_in_point)},
+ {"right": DEFAULT_PAGE_WIDTH - ceil(inch_in_cm / inch_in_point)},
+ {"bottom": DEFAULT_PAGE_HEIGHT - ceil(inch_in_cm / inch_in_point)},
+ ],
+ ids=[
+ "top",
+ "left",
+ "right",
+ "bottom",
+ ],
+)
+async def test_margin_minimum_page_size(
+ bidi_session,
+ top_context,
+ inline,
+ assert_pdf_dimensions,
+ margin,
+):
+ page = inline("Text")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=page, wait="complete"
+ )
+
+ value = await bidi_session.browsing_context.print(
+ context=top_context["context"],
+ shrink_to_fit=False,
+ margin=margin
+ )
+
+ if "top" in margin or "bottom" in margin:
+ expected_width = DEFAULT_PAGE_WIDTH
+ else:
+ expected_width = DEFAULT_PAGE_WIDTH - (inch_in_cm / inch_in_point)
+
+ if "left" in margin or "right" in margin:
+ expected_height = DEFAULT_PAGE_HEIGHT
+ else:
+ expected_height = DEFAULT_PAGE_HEIGHT - (inch_in_cm / inch_in_point)
+
+ # Check that margins don't affect page dimensions and equal defaults.
+ await assert_pdf_dimensions(value, {
+ "width": expected_width,
+ "height": expected_height,
+ })
+
+
+@pytest.mark.parametrize(
+ "margin",
+ [
+ {},
+ {"top": 0, "left": 0, "right": 0, "bottom": 0},
+ {"top": 2, "left": 2, "right": 2, "bottom": 2}
+ ],
+ ids=[
+ "default",
+ "0",
+ "2"
+ ],
+)
+async def test_margin_does_not_affect_page_size(
+ bidi_session,
+ top_context,
+ inline,
+ assert_pdf_dimensions,
+ margin
+):
+ url = inline("")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ value = await bidi_session.browsing_context.print(
+ context=top_context["context"],
+ margin=margin
+ )
+
+ # Check that margins don't affect page dimensions
+ # and equal in this case defaults.
+ await assert_pdf_dimensions(value, {
+ "width": DEFAULT_PAGE_WIDTH,
+ "height": DEFAULT_PAGE_HEIGHT,
+ })
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/orientation.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/orientation.py
new file mode 100644
index 0000000000..2e410d7430
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/orientation.py
@@ -0,0 +1,43 @@
+import pytest
+
+from tests.support.asserts import assert_pdf
+from tests.support.image import png_dimensions
+
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "orientation_value, is_portrait",
+ [
+ (None, True),
+ ("portrait", True),
+ ("landscape", False),
+ ],
+ ids=[
+ "default",
+ "portrait",
+ "landscape",
+ ],
+)
+async def test_orientation(
+ bidi_session,
+ top_context,
+ inline,
+ render_pdf_to_png_bidi,
+ orientation_value,
+ is_portrait,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline(""), wait="complete"
+ )
+ print_value = await bidi_session.browsing_context.print(
+ context=top_context["context"], orientation=orientation_value
+ )
+
+ assert_pdf(print_value)
+
+ png = await render_pdf_to_png_bidi(print_value)
+ width, height = png_dimensions(png)
+
+ assert (width < height) == is_portrait
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page.py
new file mode 100644
index 0000000000..ef1c07d142
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page.py
@@ -0,0 +1,39 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "page, orientation, expected_dimensions",
+ [
+ (None, "portrait", {"width": 21.59, "height": 27.94}),
+ ({}, "portrait", {"width": 21.59, "height": 27.94}),
+ ({"width": 4.5}, "portrait", {"width": 4.5, "height": 27.94}),
+ ({"height": 23}, "portrait", {"width": 21.59, "height": 23}),
+ ({"width": 4.5, "height": 12}, "portrait", {"width": 4.5, "height": 12}),
+ ({"height": 12}, "portrait", {"width": 21.59, "height": 12}),
+ (None, "landscape", {"width": 27.94, "height": 21.59}),
+ ({}, "landscape", {"width": 27.94, "height": 21.59}),
+ ({"width": 4.5}, "landscape", {"width": 27.94, "height": 4.5}),
+ ({"height": 23}, "landscape", {"width": 23, "height": 21.59}),
+ ({"width": 4.5, "height": 12}, "landscape", {"width": 12, "height": 4.5}),
+ ({"height": 12}, "landscape", {"width": 12, "height": 21.59}),
+ ],
+)
+async def test_page(
+ bidi_session,
+ top_context,
+ inline,
+ assert_pdf_dimensions,
+ page,
+ orientation,
+ expected_dimensions,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline(""), wait="complete"
+ )
+ value = await bidi_session.browsing_context.print(
+ context=top_context["context"], page=page, orientation=orientation
+ )
+
+ await assert_pdf_dimensions(value, expected_dimensions)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page_ranges.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page_ranges.py
new file mode 100644
index 0000000000..64843d3496
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/page_ranges.py
@@ -0,0 +1,131 @@
+# META: timeout=long
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "ranges,expected",
+ [
+ (
+ ["2-4"],
+ [
+ {"type": "string", "value": "Page 2"},
+ {"type": "string", "value": "Page 3"},
+ {"type": "string", "value": "Page 4"},
+ ],
+ ),
+ (
+ ["2-4", "2-3"],
+ [
+ {"type": "string", "value": "Page 2"},
+ {"type": "string", "value": "Page 3"},
+ {"type": "string", "value": "Page 4"},
+ ],
+ ),
+ (
+ ["2-4", "3-5"],
+ [
+ {"type": "string", "value": "Page 2"},
+ {"type": "string", "value": "Page 3"},
+ {"type": "string", "value": "Page 4"},
+ {"type": "string", "value": "Page 5"},
+ ],
+ ),
+ (
+ ["9-"],
+ [
+ {"type": "string", "value": "Page 9"},
+ {"type": "string", "value": "Page 10"},
+ ],
+ ),
+ (
+ ["-2"],
+ [
+ {"type": "string", "value": "Page 1"},
+ {"type": "string", "value": "Page 2"},
+ ],
+ ),
+ (
+ [7],
+ [
+ {"type": "string", "value": "Page 7"},
+ ],
+ ),
+ (
+ ["7"],
+ [
+ {"type": "string", "value": "Page 7"},
+ ],
+ ),
+ (
+ ["-2", "9-", "7"],
+ [
+ {"type": "string", "value": "Page 1"},
+ {"type": "string", "value": "Page 2"},
+ {"type": "string", "value": "Page 7"},
+ {"type": "string", "value": "Page 9"},
+ {"type": "string", "value": "Page 10"},
+ ],
+ ),
+ (
+ ["-5", "2-"],
+ [
+ {"type": "string", "value": "Page 1"},
+ {"type": "string", "value": "Page 2"},
+ {"type": "string", "value": "Page 3"},
+ {"type": "string", "value": "Page 4"},
+ {"type": "string", "value": "Page 5"},
+ {"type": "string", "value": "Page 6"},
+ {"type": "string", "value": "Page 7"},
+ {"type": "string", "value": "Page 8"},
+ {"type": "string", "value": "Page 9"},
+ {"type": "string", "value": "Page 10"},
+ ],
+ ),
+ (
+ [],
+ [
+ {"type": "string", "value": "Page 1"},
+ {"type": "string", "value": "Page 2"},
+ {"type": "string", "value": "Page 3"},
+ {"type": "string", "value": "Page 4"},
+ {"type": "string", "value": "Page 5"},
+ {"type": "string", "value": "Page 6"},
+ {"type": "string", "value": "Page 7"},
+ {"type": "string", "value": "Page 8"},
+ {"type": "string", "value": "Page 9"},
+ {"type": "string", "value": "Page 10"},
+ ],
+ ),
+ ],
+)
+async def test_page_ranges_document(
+ bidi_session, inline, top_context, assert_pdf_content, ranges, expected
+):
+ url = inline(
+ """
+<style>
+div {page-break-after: always}
+</style>
+
+<div>Page 1</div>
+<div>Page 2</div>
+<div>Page 3</div>
+<div>Page 4</div>
+<div>Page 5</div>
+<div>Page 6</div>
+<div>Page 7</div>
+<div>Page 8</div>
+<div>Page 9</div>
+<div>Page 10</div>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ value = await bidi_session.browsing_context.print(
+ context=top_context["context"], page_ranges=ranges
+ )
+
+ await assert_pdf_content(value, expected)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/scale.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/scale.py
new file mode 100644
index 0000000000..bffc09af67
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/scale.py
@@ -0,0 +1,57 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+def get_content(css=""):
+ return f"""
+ <div></div>
+ <style>
+ html,
+ body {{
+ margin: 0;
+ }}
+ div {{
+ background-color: black;
+ {css}
+ }}
+ </style>
+ """
+
+
+@pytest.mark.parametrize(
+ "scale, reference_css",
+ [
+ (None, "width: 100px; height: 100px;"),
+ (2, "width: 200px; height: 200px;"),
+ (0.5, "width: 50px; height: 50px;"),
+ ],
+ ids=["default", "twice", "half"],
+)
+async def test_scale(
+ bidi_session,
+ top_context,
+ inline,
+ assert_pdf_image,
+ scale,
+ reference_css,
+):
+ not_scaled_content = get_content("width: 100px; height: 100px;")
+ default_content_page = inline(not_scaled_content)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=default_content_page, wait="complete"
+ )
+
+ scaled_print_value = await bidi_session.browsing_context.print(
+ context=top_context["context"],
+ shrink_to_fit=False,
+ scale=scale,
+ background=True,
+ )
+
+ # Check that pdf scaled with print command is equal pdf of scaled with css content.
+ await assert_pdf_image(scaled_print_value, get_content(reference_css), True)
+ # If scale is not None, check that pdf scaled with print command is not equal pdf with not scaled content.
+ if scale is not None:
+ await assert_pdf_image(scaled_print_value, not_scaled_content, False)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/shrink_to_fit.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/shrink_to_fit.py
new file mode 100644
index 0000000000..db355280de
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/print/shrink_to_fit.py
@@ -0,0 +1,50 @@
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "shrink_to_fit, pages_content",
+ [
+ (None, [{"type": "string", "value": "Block 1Block 2Block 3Block 4"}]),
+ (True, [{"type": "string", "value": "Block 1Block 2Block 3Block 4"}]),
+ (
+ False,
+ [
+ {"type": "string", "value": "Block 1Block 2Block 3"},
+ {"type": "string", "value": "Block 4"},
+ ],
+ ),
+ ],
+ ids=["default", "True", "False"],
+)
+async def test_shrink_to_fit(
+ bidi_session,
+ top_context,
+ inline,
+ assert_pdf_content,
+ shrink_to_fit,
+ pages_content,
+):
+ url = inline(
+ """
+ <style>
+ div {
+ width: 1200px;
+ height: 400px;
+ }
+ </style>
+ <div>Block 1</div>
+ <div>Block 2</div>
+ <div>Block 3</div>
+ <div>Block 4</div>
+ """
+ )
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+ value = await bidi_session.browsing_context.print(
+ context=top_context["context"], shrink_to_fit=shrink_to_fit
+ )
+
+ await assert_pdf_content(value, pages_content)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/__init__.py
new file mode 100644
index 0000000000..3cec19363d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/__init__.py
@@ -0,0 +1,40 @@
+import pytest
+
+from webdriver.bidi.error import UnknownErrorException
+
+from ... import any_string
+
+
+async def reload_and_assert(
+ bidi_session,
+ context,
+ expected_error=False,
+ last_navigation=None,
+ url=None,
+ wait="complete",
+):
+ if expected_error:
+ with pytest.raises(UnknownErrorException):
+ await bidi_session.browsing_context.reload(
+ context=context['context'], wait=wait
+ )
+
+ else:
+ result = await bidi_session.browsing_context.reload(
+ context=context['context'], wait=wait
+ )
+
+ any_string(result["navigation"])
+ any_string(result["url"])
+
+ if last_navigation is not None:
+ assert result["navigation"] != last_navigation
+
+ if url is not None:
+ assert result["url"] == url
+
+ contexts = await bidi_session.browsing_context.get_tree(root=context['context'])
+ assert len(contexts) == 1
+ assert contexts[0]["url"] == url
+
+ return result
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/frame.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/frame.py
new file mode 100644
index 0000000000..dd2bf6a40f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/frame.py
@@ -0,0 +1,28 @@
+import pytest
+
+from . import reload_and_assert
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_origin(bidi_session, new_tab, inline, domain):
+ frame_url = inline("frame")
+ parent_url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+
+ # Navigate and assert (top-level).
+ result = await bidi_session.browsing_context.navigate(
+ context=new_tab['context'], url=parent_url, wait="complete")
+ assert result["url"] == parent_url
+
+ contexts = await bidi_session.browsing_context.get_tree(
+ root=new_tab['context'])
+ assert len(contexts) == 1
+ assert contexts[0]["url"] == parent_url
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+ assert frame["url"] == frame_url
+
+ # Reload and assert (frame).
+ reload_and_assert(bidi_session, frame, last_navigation=result["navigation"], url=frame_url)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/invalid.py
new file mode 100644
index 0000000000..35be21ef19
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/invalid.py
@@ -0,0 +1,37 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.reload(context=value)
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_params_context_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.reload(context=value)
+
+
+@pytest.mark.parametrize("value", ["", 42, {}, []])
+async def test_params_ignore_cache_invalid_type(bidi_session, new_tab, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.reload(context=new_tab["context"],
+ ignore_cache=value)
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_wait_invalid_type(bidi_session, new_tab, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.reload(context=new_tab["context"],
+ wait=value)
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_params_wait_invalid_value(bidi_session, new_tab, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.reload(context=new_tab["context"],
+ wait=value)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/reload.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/reload.py
new file mode 100644
index 0000000000..fb5157a365
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/reload.py
@@ -0,0 +1,73 @@
+from pathlib import Path
+
+import pytest
+
+from . import reload_and_assert
+
+
+pytestmark = pytest.mark.asyncio
+
+PNG_BLACK_DOT = "/webdriver/tests/bidi/browsing_context/support/black_dot.png"
+
+
+@pytest.mark.parametrize("hash", [False, True], ids=["without hash", "with hash"])
+async def test_reload(bidi_session, inline, new_tab, hash):
+ url = inline("""<div id="foo""")
+ if hash:
+ url += "#foo"
+
+ navigate_result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete"
+ )
+
+ reload_and_assert(
+ bidi_session,
+ new_tab,
+ last_navigation=navigate_result["navigation"],
+ url=url
+ )
+
+
+@pytest.mark.parametrize(
+ "url",
+ [
+ "about:blank",
+ "data:text/html,<p>foo</p>",
+ f'{Path(__file__).parents[1].as_uri()}/support/empty.html',
+ ],
+ ids=[
+ "about:blank",
+ "data url",
+ "file url",
+ ],
+)
+async def test_reload_special_protocols(bidi_session, new_tab, url):
+ navigate_result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete"
+ )
+
+ reload_and_assert(
+ bidi_session,
+ new_tab,
+ last_navigation=navigate_result["navigation"],
+ url=url
+ )
+
+
+async def test_image(bidi_session, new_tab, url):
+ navigate_result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url(PNG_BLACK_DOT),
+ wait="complete"
+ )
+
+ reload_and_assert(
+ bidi_session,
+ new_tab,
+ last_navigation=navigate_result["navigation"],
+ url=url
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/wait.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/wait.py
new file mode 100644
index 0000000000..1024b787f0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/reload/wait.py
@@ -0,0 +1,173 @@
+# META: timeout=long
+
+import asyncio
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+async def wait_for_reload(bidi_session, context, wait, expect_timeout):
+ # Ultimately, "interactive" and "complete" should support a timeout argument.
+ # See https://github.com/w3c/webdriver-bidi/issues/188.
+ if expect_timeout:
+ with pytest.raises(asyncio.TimeoutError):
+ await asyncio.wait_for(
+ asyncio.shield(
+ bidi_session.browsing_context.reload(context=context,
+ wait=wait)),
+ timeout=1,
+ )
+ else:
+ await bidi_session.browsing_context.reload(context=context, wait=wait)
+
+
+@pytest.mark.parametrize("wait", ["none", "interactive", "complete"])
+async def test_expected_url(bidi_session, inline, new_tab, wait):
+ url = inline("<div>foo</div>")
+
+ navigate_result = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete"
+ )
+
+ reload_result = await bidi_session.browsing_context.reload(
+ context=new_tab["context"],
+ wait=wait
+ )
+
+ if wait != "none":
+ assert reload_result["navigation"] != navigate_result["navigation"]
+ assert reload_result["url"] == url
+
+ contexts = await bidi_session.browsing_context.get_tree(
+ root=new_tab["context"], max_depth=0)
+ assert contexts[0]["url"] == url
+
+
+@pytest.mark.parametrize(
+ "wait, expect_timeout",
+ [
+ ("none", False),
+ ("interactive", False),
+ ("complete", True),
+ ],
+)
+async def test_slow_image_blocks_load(bidi_session, inline, new_tab, wait,
+ expect_timeout):
+
+ image_url = "/webdriver/tests/bidi/browsing_context/support/empty.svg"
+ url = inline(f"<img src='{image_url}?pipe=trickle(d3)'>")
+
+ await bidi_session.browsing_context.navigate(context=new_tab["context"],
+ url=url,
+ wait="complete")
+
+ await wait_for_reload(bidi_session, new_tab["context"], wait,
+ expect_timeout)
+
+ # We cannot assert the URL for "none" by definition, and for "complete", since
+ # we expect a timeout. For the timeout case, the wait_for_navigation helper will
+ # resume after 1 second, there is no guarantee that the URL has been updated.
+ if wait == "interactive":
+ contexts = await bidi_session.browsing_context.get_tree(
+ root=new_tab["context"], max_depth=0)
+ assert contexts[0]["url"] == url
+
+
+@pytest.mark.parametrize(
+ "wait, expect_timeout",
+ [
+ ("none", False),
+ ("interactive", True),
+ ("complete", True),
+ ],
+)
+async def test_slow_page(bidi_session, new_tab, url, wait, expect_timeout,
+ subscribe_events, wait_for_event):
+ url = url(
+ "/webdriver/tests/bidi/browsing_context/support/empty.html?pipe=trickle(d3)"
+ )
+
+ await bidi_session.browsing_context.navigate(context=new_tab["context"],
+ url=url,
+ wait="complete")
+
+ await subscribe_events(
+ events=["browsingContext.domContentLoaded", "browsingContext.load"],
+ contexts=[new_tab["context"]])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener_1 = bidi_session.add_event_listener(
+ "browsingContext.domContentLoaded", on_event)
+ remove_listener_2 = bidi_session.add_event_listener(
+ "browsingContext.load", on_event)
+
+ assert len(events) == 0
+
+ on_dom_content_load = wait_for_event("browsingContext.domContentLoaded")
+ on_load = wait_for_event("browsingContext.load")
+
+ await wait_for_reload(bidi_session, new_tab["context"], wait,
+ expect_timeout)
+ # Note that we cannot assert the top context url here, because the navigation
+ # is blocked on the initial url for this test case.
+
+ await asyncio.gather(on_load, on_dom_content_load)
+ assert len(events) == 2
+
+ remove_listener_2()
+ remove_listener_1()
+
+
+@pytest.mark.parametrize(
+ "wait, expect_timeout",
+ [
+ ("none", False),
+ ("interactive", True),
+ ("complete", True),
+ ],
+)
+async def test_slow_script_blocks_domContentLoaded(bidi_session, inline,
+ new_tab, wait,
+ expect_timeout,
+ subscribe_events,
+ wait_for_event):
+ script_url = "/webdriver/tests/bidi/browsing_context/support/empty.js"
+ url = inline(f"<script src='{script_url}?pipe=trickle(d3)'></script>")
+
+ await bidi_session.browsing_context.navigate(context=new_tab["context"],
+ url=url,
+ wait="complete")
+
+ await subscribe_events(
+ events=["browsingContext.domContentLoaded", "browsingContext.load"],
+ contexts=[new_tab["context"]])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener_1 = bidi_session.add_event_listener(
+ "browsingContext.domContentLoaded", on_event)
+ remove_listener_2 = bidi_session.add_event_listener(
+ "browsingContext.load", on_event)
+
+ assert len(events) == 0
+
+ on_dom_content_load = wait_for_event("browsingContext.domContentLoaded")
+ on_load = wait_for_event("browsingContext.load")
+
+ await wait_for_reload(bidi_session, new_tab["context"], wait,
+ expect_timeout)
+
+ await asyncio.gather(on_dom_content_load, on_load)
+ assert len(events) == 2
+
+ remove_listener_2()
+ remove_listener_1()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py
new file mode 100644
index 0000000000..e4db779bd5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py
@@ -0,0 +1,70 @@
+import pytest
+
+from ... import get_device_pixel_ratio, get_viewport_dimensions
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("device_pixel_ratio", [0.5, 2])
+async def test_device_pixel_ratio_only(bidi_session, inline, new_tab, device_pixel_ratio):
+ viewport = await get_viewport_dimensions(bidi_session, new_tab)
+
+ # Load a page so that reflow is triggered when changing the DPR
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ device_pixel_ratio=device_pixel_ratio)
+
+ assert await get_device_pixel_ratio(bidi_session, new_tab) == device_pixel_ratio
+ assert await get_viewport_dimensions(bidi_session, new_tab) == viewport
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("device_pixel_ratio", [0.5, 2])
+async def test_device_pixel_ratio_with_viewport(
+ bidi_session, inline, new_tab, device_pixel_ratio
+):
+ test_viewport = {"width": 250, "height": 300}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != test_viewport
+
+ # Load a page so that reflow is triggered when changing the DPR
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport,
+ device_pixel_ratio=device_pixel_ratio)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+ assert await get_device_pixel_ratio(bidi_session, new_tab) == device_pixel_ratio
+
+
+@pytest.mark.asyncio
+async def test_reset_device_pixel_ratio(bidi_session, inline, new_tab):
+ original_dpr = await get_device_pixel_ratio(bidi_session, new_tab)
+ test_dpr = original_dpr + 1
+
+ # Load a page so that reflow is triggered when changing the DPR
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ device_pixel_ratio=test_dpr)
+
+ assert await get_device_pixel_ratio(bidi_session, new_tab) == test_dpr
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ device_pixel_ratio=None)
+
+ assert await get_device_pixel_ratio(bidi_session, new_tab) == original_dpr
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/invalid.py
new file mode 100644
index 0000000000..744bc0f7f7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/invalid.py
@@ -0,0 +1,91 @@
+import pytest
+
+import webdriver.bidi.error as error
+
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(context=value, viewport={
+ "width": 100,
+ "height": 200,
+ })
+
+
+async def test_params_context_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.set_viewport(context="_invalid_")
+
+
+async def test_params_context_iframe(bidi_session, new_tab, get_test_page):
+ url = get_test_page(as_frame=True)
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete")
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ assert len(contexts) == 1
+ frames = contexts[0]["children"]
+ assert len(frames) == 1
+ frame_context = frames[0]["context"]
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(context=frame_context)
+
+
+@pytest.mark.parametrize("viewport", [False, 42, "", {}, [], {"width": 100}, {"height": 100}])
+async def test_params_viewport_invalid_type(bidi_session, new_tab, viewport):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(context=new_tab["context"], viewport=viewport)
+
+
+@pytest.mark.parametrize("width", [None, False, "", 42.1, {}, []])
+async def test_params_viewport_width_invalid_type(bidi_session, new_tab, width):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(context=new_tab["context"], viewport={
+ "width": width,
+ "height": 100,
+ })
+
+
+@pytest.mark.parametrize("height", [None, False, "", 42.1, {}, []])
+async def test_params_viewport_height_invalid_type(bidi_session, new_tab, height):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(context=new_tab["context"], viewport={
+ "width": 100,
+ "height": height,
+ })
+
+
+@pytest.mark.parametrize("viewport", [
+ {"width": -1, "height": 100},
+ {"width": 100, "height": -1},
+ {"width": -1, "height": -1},
+], ids=["width negative", "height negative", "both negative"])
+async def test_params_viewport_invalid_value(bidi_session, new_tab, viewport):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(context=new_tab["context"], viewport=viewport)
+
+
+@pytest.mark.parametrize("device_pixel_ratio", [False, "", {}, []])
+async def test_params_devicePixelRatio_invalid_type(bidi_session, new_tab,device_pixel_ratio):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ device_pixel_ratio=device_pixel_ratio,
+ viewport=None
+ )
+
+
+@pytest.mark.parametrize("device_pixel_ratio", [0, -1])
+async def test_params_devicePixelRatio_invalid_value(bidi_session, new_tab, device_pixel_ratio):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ device_pixel_ratio=device_pixel_ratio,
+ viewport=None
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py
new file mode 100644
index 0000000000..60f9e47040
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/set_viewport/viewport.py
@@ -0,0 +1,186 @@
+import pytest
+from webdriver.bidi.undefined import UNDEFINED
+
+from ... import get_viewport_dimensions
+
+
+@pytest.mark.asyncio
+async def test_set_viewport(bidi_session, new_tab):
+ test_viewport = {"width": 250, "height": 300}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != test_viewport
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+
+@pytest.mark.asyncio
+async def test_undefined_viewport(bidi_session, inline, new_tab):
+ test_viewport = {"width": 499, "height": 599}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != test_viewport
+
+ # Load a page so that reflow is triggered when changing the viewport
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=UNDEFINED)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("width, height", [
+ (250, 300),
+ (500, 300),
+ (250, 600),
+ (500, 600)
+], ids=["none", "width", "height", "both"])
+async def test_modified_dimensions(bidi_session, inline, new_tab, width, height):
+ start_viewport = {"width": 250, "height": 300}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != start_viewport
+
+ # Load a page so that reflow is triggered when changing the viewport
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=start_viewport)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == start_viewport
+
+ modified_viewport = {"width": width, "height": height}
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=modified_viewport)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == modified_viewport
+
+
+@pytest.mark.asyncio
+async def test_reset_to_default(bidi_session, inline, new_tab):
+ original_viewport = await get_viewport_dimensions(bidi_session, new_tab)
+
+ test_viewport = {"width": 666, "height": 333}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != test_viewport
+
+ # Load a page so that reflow is triggered when changing the viewport
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport
+ )
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=None
+ )
+ assert await get_viewport_dimensions(bidi_session, new_tab) == original_viewport
+
+
+@pytest.mark.asyncio
+async def test_specific_context(bidi_session, inline, new_tab, top_context):
+ original_viewport = await get_viewport_dimensions(bidi_session, top_context)
+
+ test_viewport = {"width": 333, "height": 666}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != test_viewport
+
+ # Load a page so that reflow is triggered when changing the viewport
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport
+ )
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+ assert await get_viewport_dimensions(bidi_session, top_context) == original_viewport
+
+
+@pytest.mark.parametrize("protocol,parameters", [
+ ("http", ""),
+ ("https", ""),
+ ("https", {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"})
+], ids=[
+ "http",
+ "https",
+ "https coop"
+])
+@pytest.mark.asyncio
+async def test_persists_on_navigation(bidi_session, new_tab, inline, protocol, parameters):
+ test_viewport = {"width": 499, "height": 599}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != test_viewport
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+ url = inline("<div>foo</div>", parameters=parameters, protocol=protocol)
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+ url = inline("<div>bar</div>", parameters=parameters, protocol=protocol, domain="alt")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+
+@pytest.mark.asyncio
+async def test_persists_on_reload(bidi_session, inline, new_tab):
+ test_viewport = {"width": 499, "height": 599}
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) != test_viewport
+
+ # Load a page so that reflow is triggered when changing the viewport
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+
+ await bidi_session.browsing_context.set_viewport(
+ context=new_tab["context"],
+ viewport=test_viewport)
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
+
+ await bidi_session.browsing_context.reload(
+ context=new_tab["context"], wait="complete"
+ )
+
+ assert await get_viewport_dimensions(bidi_session, new_tab) == test_viewport
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/black_dot.png b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/black_dot.png
new file mode 100644
index 0000000000..613754cfaf
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/black_dot.png
Binary files differ
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.html b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.html
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.js b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.js
new file mode 100644
index 0000000000..3918c74e44
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.js
@@ -0,0 +1 @@
+"use strict";
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.svg b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.svg
new file mode 100644
index 0000000000..e0af766e8f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/empty.svg
@@ -0,0 +1,2 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+</svg>
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.html b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.html
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.svg b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.svg
new file mode 100644
index 0000000000..7c20a99a4b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/other.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <rect x="10" y="10" width="100" height="100" style="fill: LightSkyBlue" />
+</svg>
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/red_dot.png b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/red_dot.png
new file mode 100644
index 0000000000..c5916f2897
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/support/red_dot.png
Binary files differ
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/conftest.py
new file mode 100644
index 0000000000..fcd7cdf114
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/conftest.py
@@ -0,0 +1,38 @@
+import pytest_asyncio
+
+from tests.support.sync import AsyncPoll
+
+# Temporary fixtures until traverse history is fully implemented and will await the navigation.
+# See: https://github.com/w3c/webdriver-bidi/issues/94
+
+
+@pytest_asyncio.fixture
+async def wait_for_url(bidi_session, current_url):
+ async def wait_for_url(context, target_url, timeout=2):
+ async def check_url(_):
+ return await current_url(context) == target_url
+
+ wait = AsyncPoll(
+ bidi_session,
+ timeout=timeout,
+ message="Expected URL did not load"
+ )
+ await wait.until(check_url)
+
+ return wait_for_url
+
+
+@pytest_asyncio.fixture
+async def wait_for_not_url(bidi_session, current_url):
+ async def wait_for_not_url(context, target_url, timeout=2):
+ async def check_url(_):
+ return await current_url(context) != target_url
+
+ wait = AsyncPoll(
+ bidi_session,
+ timeout=timeout,
+ message="Expected URL is still loaded"
+ )
+ await wait.until(check_url)
+
+ return wait_for_not_url
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/context.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/context.py
new file mode 100644
index 0000000000..2635dcfa28
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/context.py
@@ -0,0 +1,55 @@
+import pytest
+
+import webdriver.bidi.error as error
+
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_top_level_contexts(
+ bidi_session, current_url, wait_for_url, top_context, new_tab, inline
+):
+ pages = [
+ inline("<div>page 1</div>"),
+ inline("<div>page 2</div>"),
+ ]
+ for page in pages:
+ for context in [top_context["context"], new_tab["context"]]:
+ await bidi_session.browsing_context.navigate(
+ context=context, url=page, wait="complete"
+ )
+ assert await current_url(context) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ await wait_for_url(top_context["context"], pages[1])
+ await wait_for_url(new_tab["context"], pages[0])
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_iframe(bidi_session, current_url, wait_for_url, new_tab, inline, domain):
+ iframe_url_1 = inline("page 1")
+ page_url = inline(f"<iframe src='{iframe_url_1}'></iframe>", domain=domain)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page_url, wait="complete"
+ )
+ assert await current_url(new_tab["context"]) == page_url
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ iframe_context = contexts[0]["children"][0]
+
+ iframe_url_2 = inline("page 2")
+ await bidi_session.browsing_context.navigate(
+ context=iframe_context["context"], url=iframe_url_2, wait="complete"
+ )
+ assert await current_url(iframe_context["context"]) == iframe_url_2
+
+ await bidi_session.browsing_context.traverse_history(
+ context=iframe_context["context"], delta=-1
+ )
+
+ await wait_for_url(new_tab["context"], page_url)
+ await wait_for_url(iframe_context["context"], iframe_url_1)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/delta.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/delta.py
new file mode 100644
index 0000000000..3bd0087250
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/delta.py
@@ -0,0 +1,167 @@
+from pathlib import Path
+
+import pytest
+from webdriver import error
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.sync import AsyncPoll
+
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_delta_0(
+ bidi_session, current_url, wait_for_url, wait_for_not_url, new_tab, inline
+):
+ pages = [
+ inline("<div>page 1</div>"),
+ inline("<div>page 2</div>"),
+ inline("<div>page 3</div>"),
+ ]
+ for page in pages:
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await current_url(new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+ await wait_for_url(new_tab["context"], pages[1])
+
+ # With delta 0 no navigation has to happen
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=0
+ )
+ with pytest.raises(error.TimeoutException):
+ await wait_for_not_url(new_tab["context"], pages[1])
+
+
+async def test_delta_forward_and_back(
+ bidi_session, current_url, wait_for_url, new_tab, inline
+):
+ pages = [
+ inline("<div>page 1</div>"),
+ inline("<div>page 2</div>"),
+ inline("<div>page 3</div>"),
+ ]
+ for page in pages:
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await current_url(new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-2
+ )
+
+ await wait_for_url(new_tab["context"], pages[0])
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=2
+ )
+
+ await wait_for_url(new_tab["context"], pages[2])
+
+
+async def test_navigate_in_the_same_document(
+ bidi_session, current_url, wait_for_url, new_tab, url
+):
+ page_url = "/webdriver/tests/bidi/browsing_context/support/empty.html"
+ pages = [
+ url(page_url),
+ url(page_url + "#foo"),
+ url(page_url + "#bar"),
+ ]
+ for page in pages:
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await current_url(new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ await wait_for_url(new_tab["context"], pages[1])
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=1
+ )
+
+ await wait_for_url(new_tab["context"], pages[2])
+
+
+async def test_history_push_state(
+ bidi_session, current_url, wait_for_url, new_tab, url
+):
+ page_url = url("/webdriver/tests/bidi/browsing_context/support/empty.html")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page_url, wait="complete"
+ )
+ assert await current_url(new_tab["context"]) == page_url
+
+ pages = [
+ f"{page_url}#foo",
+ f"{page_url}#bar",
+ ]
+ for page in pages:
+ await bidi_session.script.call_function(
+ function_declaration="""(url) => {
+ history.pushState(null, null, url);
+ }""",
+ arguments=[
+ {"type": "string", "value": page},
+ ],
+ await_promise=False,
+ target=ContextTarget(new_tab["context"]),
+ )
+ await wait_for_url(new_tab["context"], page)
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ await wait_for_url(new_tab["context"], pages[0])
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=1
+ )
+
+ await wait_for_url(new_tab["context"], pages[1])
+
+
+@pytest.mark.parametrize(
+ "pages",
+ [
+ ["data:text/html,<p>foo</p>", "data:text/html,<p>bar</p>"],
+ [
+ f"{Path(__file__).parents[1].as_uri()}/support/empty.html",
+ f"{Path(__file__).parents[1].as_uri()}/support/other.html",
+ ],
+ ],
+ ids=[
+ "data url",
+ "file url",
+ ],
+)
+async def test_navigate_special_protocols(
+ bidi_session, current_url, wait_for_url, new_tab, pages
+):
+ for page in pages:
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await current_url(new_tab["context"]) == page
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=-1
+ )
+
+ await wait_for_url(new_tab["context"], pages[0])
+
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=1
+ )
+
+ await wait_for_url(new_tab["context"], pages[1])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/invalid.py
new file mode 100644
index 0000000000..c6a90601e7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/traverse_history/invalid.py
@@ -0,0 +1,44 @@
+import pytest
+import webdriver.bidi.error as error
+
+
+pytestmark = pytest.mark.asyncio
+
+
+MAX_INT = 9007199254740991
+MIN_INT = -MAX_INT
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.traverse_history(context=value, delta=1)
+
+
+async def test_params_context_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.browsing_context.traverse_history(context="foo", delta=1)
+
+
+@pytest.mark.parametrize(
+ "value", [None, False, "foo", 1.5, MIN_INT - 1, MAX_INT + 1, {}, []]
+)
+async def test_params_delta_invalid_type(bidi_session, top_context, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.browsing_context.traverse_history(
+ context=top_context["context"], delta=value
+ )
+
+
+@pytest.mark.parametrize("value", [-2, 1])
+async def test_delta_invalid_value(bidi_session, current_url, new_tab, inline, value):
+ page = inline("<div>page 1</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page, wait="complete"
+ )
+ assert await current_url(new_tab["context"]) == page
+
+ with pytest.raises(error.NoSuchHistoryEntryException):
+ await bidi_session.browsing_context.traverse_history(
+ context=new_tab["context"], delta=value
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/user_prompt_closed.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/user_prompt_closed.py
new file mode 100644
index 0000000000..68a0eed192
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_closed/user_prompt_closed.py
@@ -0,0 +1,270 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.error import TimeoutException
+
+pytestmark = pytest.mark.asyncio
+
+USER_PROMPT_CLOSED_EVENT = "browsingContext.userPromptClosed"
+USER_PROMPT_OPENED_EVENT = "browsingContext.userPromptOpened"
+
+
+async def test_unsubscribe(bidi_session, inline, new_tab, wait_for_event, wait_for_future_safe):
+ await bidi_session.session.subscribe(
+ events=[USER_PROMPT_CLOSED_EVENT, USER_PROMPT_OPENED_EVENT]
+ )
+ await bidi_session.session.unsubscribe(events=[USER_PROMPT_CLOSED_EVENT])
+
+ on_entry = wait_for_event("browsingContext.userPromptOpened")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline("<script>window.alert('test')</script>"),
+ )
+
+ # Wait for the alert to open
+ await wait_for_future_safe(on_entry)
+
+ # Track all received browsingContext.userPromptClosed events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ USER_PROMPT_CLOSED_EVENT, on_event
+ )
+
+ await bidi_session.browsing_context.handle_user_prompt(context=new_tab["context"])
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+async def test_subscribe_with_alert(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[USER_PROMPT_CLOSED_EVENT, USER_PROMPT_OPENED_EVENT])
+
+ on_prompt_opened = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline("<script>window.alert('test')</script>"),
+ )
+
+ # Wait for the prompt to open.
+ await wait_for_future_safe(on_prompt_opened)
+
+ on_prompt_closed = wait_for_event(USER_PROMPT_CLOSED_EVENT)
+
+ await bidi_session.browsing_context.handle_user_prompt(context=new_tab["context"])
+
+ event = await wait_for_future_safe(on_prompt_closed)
+
+ assert event == {"context": new_tab["context"], "accepted": True}
+
+
+@pytest.mark.parametrize("accept", [True, False])
+async def test_subscribe_with_confirm(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe, accept
+):
+ await subscribe_events(events=[USER_PROMPT_CLOSED_EVENT, USER_PROMPT_OPENED_EVENT])
+
+ on_prompt_opened = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline("<script>window.confirm('test')</script>"),
+ )
+
+ # Wait for the prompt to open.
+ await wait_for_future_safe(on_prompt_opened)
+
+ on_prompt_closed = wait_for_event(USER_PROMPT_CLOSED_EVENT)
+
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=new_tab["context"], accept=accept
+ )
+
+ event = await wait_for_future_safe(on_prompt_closed)
+
+ assert event == {"context": new_tab["context"], "accepted": accept}
+
+
+@pytest.mark.parametrize("accept", [True, False])
+async def test_subscribe_with_prompt(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe, accept
+):
+ await subscribe_events(events=[USER_PROMPT_CLOSED_EVENT, USER_PROMPT_OPENED_EVENT])
+
+ on_prompt_opened = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline("<script>window.prompt('Enter Your Name: ')</script>"),
+ )
+
+ # Wait for the prompt to open.
+ await wait_for_future_safe(on_prompt_opened)
+
+ on_prompt_closed = wait_for_event(USER_PROMPT_CLOSED_EVENT)
+
+ test_user_text = "Test"
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=new_tab["context"], accept=accept, user_text=test_user_text
+ )
+
+ event = await wait_for_future_safe(on_prompt_closed)
+
+ if accept is True:
+ assert event == {
+ "context": new_tab["context"],
+ "accepted": accept,
+ "userText": test_user_text,
+ }
+ else:
+ assert event == {"context": new_tab["context"], "accepted": accept}
+
+
+async def test_subscribe_with_prompt_with_defaults(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=[USER_PROMPT_CLOSED_EVENT, USER_PROMPT_OPENED_EVENT])
+
+ on_prompt_opened = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline("<script>window.prompt('Enter Your Name: ')</script>"),
+ )
+
+ # Wait for the prompt to open.
+ await wait_for_future_safe(on_prompt_opened)
+
+ on_prompt_closed = wait_for_event(USER_PROMPT_CLOSED_EVENT)
+
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=new_tab["context"]
+ )
+
+ event = await wait_for_future_safe(on_prompt_closed)
+
+ assert event == {"context": new_tab["context"], "accepted": True}
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_subscribe_to_one_context(
+ bidi_session, subscribe_events, inline, wait_for_event, wait_for_future_safe, type_hint
+):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ # Subscribe to open events for all contexts.
+ await subscribe_events(events=[USER_PROMPT_OPENED_EVENT])
+
+ # Subscribe to close events for only one context.
+ await subscribe_events(
+ events=[USER_PROMPT_CLOSED_EVENT],
+ contexts=[new_context["context"]],
+ )
+ # Track all received browsingContext.userPromptClosed events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ USER_PROMPT_CLOSED_EVENT, on_event
+ )
+
+ on_prompt_opened = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ another_new_context = await bidi_session.browsing_context.create(
+ type_hint=type_hint
+ )
+
+ # Open a prompt in the different context.
+ await bidi_session.browsing_context.navigate(
+ context=another_new_context["context"],
+ url=inline("<script>window.alert('second tab')</script>"),
+ )
+
+ await wait_for_future_safe(on_prompt_opened)
+
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=another_new_context["context"]
+ )
+
+ # Make sure we don't receive this event.
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ on_prompt_opened = wait_for_event(USER_PROMPT_OPENED_EVENT)
+ on_prompt_closed = wait_for_event(USER_PROMPT_CLOSED_EVENT)
+
+ # Open a prompt in the subscribed context.
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=inline("<script>window.alert('first tab')</script>"),
+ )
+
+ await wait_for_future_safe(on_prompt_opened)
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=new_context["context"]
+ )
+
+ event = await wait_for_future_safe(on_prompt_closed)
+
+ assert event == {
+ "context": new_context["context"],
+ "accepted": True,
+ }
+
+ remove_listener()
+ await bidi_session.browsing_context.close(context=new_context["context"])
+ await bidi_session.browsing_context.close(context=another_new_context["context"])
+
+
+async def test_iframe(
+ bidi_session,
+ new_tab,
+ inline,
+ test_origin,
+ subscribe_events,
+ wait_for_event,
+ wait_for_future_safe,
+):
+ await subscribe_events(events=[USER_PROMPT_CLOSED_EVENT, USER_PROMPT_OPENED_EVENT])
+
+ on_prompt_opened = wait_for_event(USER_PROMPT_OPENED_EVENT)
+ on_prompt_closed = wait_for_event(USER_PROMPT_CLOSED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline(f"<iframe src='{test_origin}'>"),
+ wait="complete",
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ assert len(contexts) == 1
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+
+ await bidi_session.browsing_context.navigate(
+ context=frame["context"],
+ url=inline("<script>window.alert('in iframe')</script>"),
+ )
+
+ await wait_for_future_safe(on_prompt_opened)
+
+ await bidi_session.browsing_context.handle_user_prompt(
+ context=frame["context"]
+ )
+
+ event = await wait_for_future_safe(on_prompt_closed)
+
+ assert event == {"context": new_tab["context"], "accepted": True}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/user_prompt_opened.py b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/user_prompt_opened.py
new file mode 100644
index 0000000000..fcd030116d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/browsing_context/user_prompt_opened/user_prompt_opened.py
@@ -0,0 +1,183 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.error import TimeoutException
+
+pytestmark = pytest.mark.asyncio
+
+USER_PROMPT_OPENED_EVENT = "browsingContext.userPromptOpened"
+
+
+async def test_unsubscribe(bidi_session, inline, new_tab):
+ await bidi_session.session.subscribe(events=[USER_PROMPT_OPENED_EVENT])
+ await bidi_session.session.unsubscribe(events=[USER_PROMPT_OPENED_EVENT])
+
+ # Track all received browsingContext.userPromptOpened events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ USER_PROMPT_OPENED_EVENT, on_event
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline("<script>window.alert('test')</script>"),
+ )
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("prompt_type", ["alert", "confirm", "prompt"])
+async def test_prompt_type(
+ bidi_session, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe, prompt_type
+):
+ await subscribe_events(events=[USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ text = "test"
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline(f"<script>window.{prompt_type}('{text}')</script>"),
+ )
+
+ event = await wait_for_future_safe(on_entry)
+
+ assert event == {
+ "context": new_tab["context"],
+ "type": prompt_type,
+ "message": text,
+ }
+
+
+@pytest.mark.parametrize(
+ "default", [None, "", "default"], ids=["null", "empty string", "non empty string"]
+)
+async def test_prompt_default_value(
+ bidi_session, inline, new_tab, subscribe_events, wait_for_event, wait_for_future_safe, default
+):
+ await subscribe_events(events=[USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ text = "test"
+
+ if default is None:
+ script = f"<script>window.prompt('{text}', null)</script>"
+ else:
+ script = f"<script>window.prompt('{text}', '{default}')</script>"
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline(script),
+ )
+
+ event = await wait_for_future_safe(on_entry)
+
+ expected_event = {
+ "context": new_tab["context"],
+ "type": "prompt",
+ "message": text,
+ }
+
+ if default is not None:
+ expected_event["defaultValue"] = default
+
+ assert event == expected_event
+
+
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_subscribe_to_one_context(
+ bidi_session, subscribe_events, inline, wait_for_event, wait_for_future_safe, type_hint
+):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ await subscribe_events(
+ events=[USER_PROMPT_OPENED_EVENT], contexts=[new_context["context"]]
+ )
+ # Track all received browsingContext.userPromptOpened events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ USER_PROMPT_OPENED_EVENT, on_event
+ )
+
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ another_new_context = await bidi_session.browsing_context.create(
+ type_hint=type_hint
+ )
+
+ # Open a prompt in the different context.
+ await bidi_session.browsing_context.navigate(
+ context=another_new_context["context"],
+ url=inline("<script>window.alert('second tab')</script>"),
+ )
+
+ # Make sure we don't receive this event.
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ # Open a prompt in the subscribed context.
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=inline("<script>window.alert('first tab')</script>"),
+ )
+
+ event = await wait_for_future_safe(on_entry)
+
+ assert event == {
+ "context": new_context["context"],
+ "type": "alert",
+ "message": "first tab",
+ }
+
+ remove_listener()
+ await bidi_session.browsing_context.close(context=new_context["context"])
+ await bidi_session.browsing_context.close(context=another_new_context["context"])
+
+
+async def test_iframe(
+ bidi_session,
+ new_tab,
+ inline,
+ test_origin,
+ subscribe_events,
+ wait_for_event,
+ wait_for_future_safe,
+):
+ await subscribe_events([USER_PROMPT_OPENED_EVENT])
+ on_entry = wait_for_event(USER_PROMPT_OPENED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=inline(f"<iframe src='{test_origin}'>"),
+ wait="complete",
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ assert len(contexts) == 1
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+
+ await bidi_session.browsing_context.navigate(
+ context=frame["context"],
+ url=inline("<script>window.alert('in iframe')</script>"),
+ )
+
+ event = await wait_for_future_safe(on_entry)
+
+ assert event == {
+ "context": new_tab["context"],
+ "type": "alert",
+ "message": "in iframe",
+ }
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/errors/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/errors/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/errors/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/errors/errors.py b/testing/web-platform/tests/webdriver/tests/bidi/errors/errors.py
new file mode 100644
index 0000000000..b54f26b8c9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/errors/errors.py
@@ -0,0 +1,16 @@
+import pytest
+
+from webdriver.bidi.error import UnknownCommandException
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("module_name, command_name", [
+ ("invalidmodule", "somecommand"),
+ ("session", "wrongcommand"),
+], ids=[
+ 'invalid module',
+ 'invalid command name',
+])
+async def test_unknown_command(send_blocking_command, module_name, command_name):
+ with pytest.raises(UnknownCommandException):
+ await send_blocking_command(f"{module_name}.{command_name}", {})
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/__init__.py
new file mode 100644
index 0000000000..b8f6358d61
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/__init__.py
@@ -0,0 +1,23 @@
+from typing import Any, Mapping
+
+from webdriver.bidi.modules.script import ContextTarget
+
+async def get_permission_state(bidi_session, context: Mapping[str, Any], name: str) -> str:
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => {
+ return navigator.permissions.query({ name: '%s' })
+ .then(val => val.state, err => err.message)
+ }""" % name,
+ target=ContextTarget(context["context"]),
+ await_promise=True)
+ return result["value"]
+
+
+async def get_context_origin(bidi_session, context: Mapping[str, Any]) -> str:
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => {
+ return window.location.origin;
+ }""",
+ target=ContextTarget(context["context"]),
+ await_promise=False)
+ return result["value"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py
new file mode 100644
index 0000000000..0ef8c57f41
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/invalid.py
@@ -0,0 +1,54 @@
+import pytest
+import webdriver.bidi.error as error
+from webdriver.bidi.undefined import UNDEFINED
+
+pytestmark = pytest.mark.asyncio
+
+@pytest.mark.parametrize("descriptor", [False, "SOME_STRING", 42, {}, [], {"name": 23}, None, UNDEFINED])
+async def test_params_descriptor_invalid_type(bidi_session, descriptor):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.permissions.set_permission(
+ descriptor=descriptor,
+ state="granted",
+ origin="https://example.com",
+ )
+
+
+@pytest.mark.parametrize("descriptor", [{"name": "unknown"}])
+async def test_params_descriptor_invalid_value(bidi_session, descriptor):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.permissions.set_permission(
+ descriptor=descriptor,
+ state="granted",
+ origin="https://example.com",
+ )
+
+
+@pytest.mark.parametrize("state", [False, 42, {}, [], None, UNDEFINED])
+async def test_params_state_invalid_type(bidi_session, state):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state=state,
+ origin="https://example.com",
+ )
+
+
+@pytest.mark.parametrize("state", ["UNKOWN", "Granted"])
+async def test_params_state_invalid_value(bidi_session, state):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state=state,
+ origin="https://example.com",
+ )
+
+
+@pytest.mark.parametrize("origin", [False, 42, {}, [], None, UNDEFINED])
+async def test_params_origin_invalid_type(bidi_session, origin):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state="granted",
+ origin=origin,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py
new file mode 100644
index 0000000000..dc6ca14a8d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/external/permissions/set_permission/set_permission.py
@@ -0,0 +1,105 @@
+import pytest
+import webdriver.bidi.error as error
+
+from . import get_context_origin, get_permission_state
+
+pytestmark = pytest.mark.asyncio
+
+@pytest.mark.asyncio
+async def test_set_permission(bidi_session, new_tab, url):
+ test_url = url("/common/blank.html", protocol="https")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=test_url,
+ wait="complete",
+ )
+
+ origin = await get_context_origin(bidi_session, new_tab)
+
+ assert await get_permission_state(bidi_session, new_tab, "geolocation") == "prompt"
+
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state="granted",
+ origin=origin,
+ )
+
+ assert await get_permission_state(bidi_session, new_tab, "geolocation") == "granted"
+
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state="denied",
+ origin=origin,
+ )
+
+ assert await get_permission_state(bidi_session, new_tab, "geolocation") == "denied"
+
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state="prompt",
+ origin=origin,
+ )
+
+ assert await get_permission_state(bidi_session, new_tab, "geolocation") == "prompt"
+
+
+@pytest.mark.asyncio
+async def test_set_permission_insecure_context(bidi_session, new_tab, url):
+ test_url = url("/common/blank.html", protocol="http")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=test_url,
+ wait="complete",
+ )
+
+ origin = await get_context_origin(bidi_session, new_tab)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "push"},
+ state="granted",
+ origin=origin,
+ )
+
+@pytest.mark.asyncio
+async def test_set_permission_new_context(bidi_session, new_tab, url):
+ test_url = url("/common/blank.html", protocol="https")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=test_url,
+ wait="complete",
+ )
+
+ origin = await get_context_origin(bidi_session, new_tab)
+
+ assert await get_permission_state(bidi_session, new_tab, "geolocation") == "prompt"
+
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state="granted",
+ origin=origin,
+ )
+
+ assert await get_permission_state(bidi_session, new_tab, "geolocation") == "granted"
+
+ new_context = await bidi_session.browsing_context.create(type_hint="tab")
+ assert new_tab["context"] != new_context["context"]
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=test_url,
+ wait="complete",
+ )
+
+ # See https://github.com/w3c/permissions/issues/437.
+ assert await get_permission_state(bidi_session, new_context, "geolocation") == "granted"
+
+
+@pytest.mark.parametrize("origin", ['UNKNOWN', ''])
+async def test_set_permission_origin_unknown(bidi_session, new_tab, origin):
+ await bidi_session.permissions.set_permission(
+ descriptor={"name": "geolocation"},
+ state="granted",
+ origin=origin,
+ )
+ assert await get_permission_state(bidi_session, new_tab, "geolocation") == "prompt"
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/input/__init__.py
new file mode 100644
index 0000000000..809379d56d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/__init__.py
@@ -0,0 +1,42 @@
+import json
+
+from webdriver.bidi.modules.script import ContextTarget
+
+async def get_object_from_context(bidi_session, context, object_path):
+ """Return a plain JS object from a given context, accessible at the given object_path"""
+ events_str = await bidi_session.script.evaluate(
+ expression=f"JSON.stringify({object_path})",
+ target=ContextTarget(context),
+ await_promise=False,
+ )
+ return json.loads(events_str["value"])
+
+
+async def get_events(bidi_session, context):
+ """Return list of key events recorded on the test_actions.html page."""
+ events = await get_object_from_context(bidi_session, context, "allEvents.events")
+
+ # `key` values in `allEvents` may be escaped (see `escapeSurrogateHalf` in
+ # test_actions.html), so this converts them back into unicode literals.
+ for e in events:
+ # example: turn "U+d83d" (6 chars) into u"\ud83d" (1 char)
+ if "key" in e and e["key"].startswith("U+"):
+ key = e["key"]
+ hex_suffix = key[key.index("+") + 1:]
+ e["key"] = chr(int(hex_suffix, 16))
+
+ # WebKit sets code as 'Unidentified' for unidentified key codes, but
+ # tests expect ''.
+ if "code" in e and e["code"] == "Unidentified":
+ e["code"] = ""
+ return events
+
+
+async def get_keys_value(bidi_session, context):
+ keys_value = await bidi_session.script.evaluate(
+ expression="""document.getElementById("keys").value""",
+ target=ContextTarget(context),
+ await_promise=False,
+ )
+
+ return keys_value["value"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/input/conftest.py
new file mode 100644
index 0000000000..4cb61f8820
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/conftest.py
@@ -0,0 +1,45 @@
+import pytest
+import pytest_asyncio
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.fixture
+def get_focused_key_input(bidi_session, top_context):
+ """Get focused input element, containing pressed key data."""
+
+ async def get_focused_key_input(context=top_context):
+ return await bidi_session.script.call_function(
+ function_declaration="""() => {
+ const elem = document.getElementById("keys");
+ elem.focus();
+ return elem;
+ }""",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ return get_focused_key_input
+
+
+@pytest_asyncio.fixture(autouse=True)
+async def release_actions(bidi_session, top_context):
+ # release all actions after each test
+ yield
+ await bidi_session.input.release_actions(context=top_context["context"])
+
+
+@pytest_asyncio.fixture
+async def setup_key_test(load_static_test_page, get_focused_key_input):
+ await load_static_test_page(page="test_actions.html")
+ await get_focused_key_input()
+
+
+@pytest_asyncio.fixture
+async def setup_wheel_test(bidi_session, top_context, load_static_test_page):
+ await load_static_test_page(page="test_actions_scroll.html")
+ await bidi_session.script.evaluate(
+ expression="document.scrollingElement.scrollTop = 0",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/__init__.py
new file mode 100644
index 0000000000..c043e0ae7e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/__init__.py
@@ -0,0 +1,137 @@
+from webdriver.bidi.modules.script import ContextTarget
+
+from .. import get_object_from_context
+
+
+def remote_mapping_to_dict(js_object):
+ obj = {}
+ for key, value in js_object:
+ obj[key] = value["value"]
+
+ return obj
+
+
+async def assert_pointer_events(
+ bidi_session, context, expected_events, target, pointer_type
+):
+ events = await get_object_from_context(
+ bidi_session, context["context"], "window.recordedEvents"
+ )
+
+ assert len(events) == len(expected_events)
+ event_types = [e["type"] for e in events]
+ assert expected_events == event_types
+
+ for e in events:
+ assert e["target"] == target
+ assert e["pointerType"] == pointer_type
+
+
+
+async def get_inview_center_bidi(bidi_session, context, element):
+ elem_rect = await get_element_rect(bidi_session,
+ context=context,
+ element=element)
+ viewport_rect = await get_viewport_rect(bidi_session,
+ context=context)
+
+ x = {
+ "left": max(0, min(elem_rect["x"],
+ elem_rect["x"] + elem_rect["width"])),
+ "right": min(
+ viewport_rect["width"],
+ max(elem_rect["x"], elem_rect["x"] + elem_rect["width"]),
+ ),
+ }
+
+ y = {
+ "top": max(0, min(elem_rect["y"],
+ elem_rect["y"] + elem_rect["height"])),
+ "bottom": min(
+ viewport_rect["height"],
+ max(elem_rect["y"], elem_rect["y"] + elem_rect["height"]),
+ ),
+ }
+
+ return {
+ "x": (x["left"] + x["right"]) / 2,
+ "y": (y["top"] + y["bottom"]) / 2,
+ }
+
+
+async def get_element_rect(bidi_session, context, element):
+ result = await bidi_session.script.call_function(
+ function_declaration="""
+el => el.getBoundingClientRect().toJSON()
+""",
+ arguments=[element],
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ return remote_mapping_to_dict(result["value"])
+
+
+async def get_shadow_root_from_test_page(bidi_session, context, nested=False):
+ custom_element = await bidi_session.script.call_function(
+ function_declaration="""() => document.querySelector("custom-element")""",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ shadow_root = custom_element["value"]["shadowRoot"]
+
+ if nested:
+ custom_element = await bidi_session.script.call_function(
+ function_declaration="""shadowRoot => shadowRoot.querySelector("inner-custom-element")""",
+ target=ContextTarget(context["context"]),
+ arguments=[shadow_root],
+ await_promise=False,
+ )
+ shadow_root = custom_element["value"]["shadowRoot"]
+
+ return shadow_root
+
+
+async def get_viewport_rect(bidi_session, context):
+ expression = """
+ ({
+ height: window.innerHeight || document.documentElement.clientHeight,
+ width: window.innerWidth || document.documentElement.clientWidth,
+ });
+ """
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ return remote_mapping_to_dict(result["value"])
+
+
+async def record_pointer_events(bidi_session, context, container, selector):
+ # Record basic mouse / pointer events on the element matching the given
+ # selector in the container.
+ # The serialized element will be returned
+ target = await bidi_session.script.call_function(
+ function_declaration=f"""container => {{
+ const target = container.querySelector("{selector}");
+ window.recordedEvents = [];
+ function onPointerEvent(event) {{
+ window.recordedEvents.push({{
+ "type": event.type,
+ "pointerType": event.pointerType,
+ "target": event.target.id
+ }});
+ }}
+ target.addEventListener("pointerdown", onPointerEvent);
+ target.addEventListener("pointerup", onPointerEvent);
+ return target;
+ }}
+ """,
+ arguments=[container],
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+
+ return target
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/invalid.py
new file mode 100644
index 0000000000..4ae3039f47
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/invalid.py
@@ -0,0 +1,904 @@
+import pytest
+import pytest_asyncio
+
+from webdriver.bidi.modules.input import Actions
+from webdriver.bidi.error import InvalidArgumentException
+
+pytestmark = pytest.mark.asyncio
+
+MAX_INT = 9007199254740991
+MIN_INT = -MAX_INT
+
+
+@pytest_asyncio.fixture
+async def perform_actions(bidi_session, top_context):
+ async def perform_actions(actions, context=top_context["context"]):
+ return await bidi_session.input.perform_actions(actions=actions,
+ context=context)
+
+ yield perform_actions
+
+
+def create_key_action(key_action, overrides=None, removals=None):
+ action = {
+ "type": key_action,
+ "value": "",
+ }
+
+ if overrides is not None:
+ action.update(overrides)
+
+ if removals is not None:
+ for removal in removals:
+ del action[removal]
+
+ return action
+
+
+def create_pointer_action(pointer_action, overrides=None, removals=None):
+ action = {
+ "type": pointer_action,
+ "width": 0,
+ "height": 0,
+ "pressure": 0.0,
+ "tangentialPressure": 0.0,
+ "twist": 0,
+ "altitudeAngle": 0,
+ "azimuthAngle": 0,
+ }
+
+ if pointer_action == "pointerMove":
+ action.update({"x": 0, "y": 0})
+ elif pointer_action in ["pointerDown", "pointerUp"]:
+ action.update({"button": 0})
+
+ if overrides is not None:
+ action.update(overrides)
+
+ if removals is not None:
+ for removal in removals:
+ del action[removal]
+
+ return action
+
+
+def create_wheel_action(wheel_action, overrides=None, removals=None):
+ action = {
+ "type": wheel_action,
+ "x": 0,
+ "y": 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ "deltaZ": 0,
+ "deltaMode": 0,
+ "origin": "viewport",
+ }
+
+ if overrides is not None:
+ action.update(overrides)
+
+ if removals is not None:
+ for removal in removals:
+ del action[removal]
+
+ return action
+
+
+@pytest.mark.parametrize("value", [None, True, 42, {}, []])
+async def test_params_context_invalid_type(perform_actions, value):
+ actions = Actions()
+ actions.add_key()
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions, context=value)
+
+
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_actions_invalid_type(
+ perform_actions, value):
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(value)
+
+
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_action_sequence_invalid_type(
+ perform_actions, value):
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([value])
+
+
+async def test_params_input_source_action_sequence_type_missing(
+ perform_actions):
+ actions = [{
+ "id": "foo",
+ "actions": [],
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+async def test_params_input_source_action_sequence_id_missing(
+ perform_actions, action_type):
+ actions = [{
+ "type": action_type,
+ "actions": [],
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+async def test_params_input_source_action_sequence_actions_missing(
+ perform_actions, action_type):
+ actions = [{
+ "type": action_type,
+ "id": "foo",
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_type_invalid_type(
+ perform_actions, value):
+ actions = [{
+ "type": value,
+ "id": "foo",
+ "actions": [],
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type",
+ ["", "nones", "keys", "pointers", "wheels"])
+async def test_params_input_source_action_sequence_type_invalid_value(
+ perform_actions, action_type):
+ actions = [{
+ "type": action_type,
+ "id": "foo",
+ "actions": [],
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_id_invalid_type(
+ perform_actions, action_type, value):
+ actions = [{
+ "type": action_type,
+ "id": value,
+ "actions": [],
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_action_sequence_actions_invalid_type(
+ perform_actions, action_type, value):
+ actions = [{
+ "type": action_type,
+ "id": "foo",
+ "actions": value,
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_action_sequence_actions_actions_invalid_type(
+ perform_actions, action_type, value):
+ actions = [{
+ "type": action_type,
+ "id": "foo",
+ "actions": [value],
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", [None, "foo", True, 42, []])
+async def test_params_input_source_action_sequence_pointer_parameters_invalid_type(
+ perform_actions, value):
+ actions = [{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [],
+ "parameters": value
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_pointer_parameters_pointer_type_invalid_type(
+ perform_actions, value):
+ actions = [{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [],
+ "parameters": {
+ "pointerType": value,
+ },
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", ["", "mouses", "pens", "touchs"])
+async def test_params_input_source_action_sequence_pointer_parameters_pointer_type_invalid_value(
+ perform_actions, value):
+ actions = [{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [],
+ "parameters": {
+ "pointerType": value,
+ },
+ }]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_actions_type_invalid_type(
+ perform_actions, action_type, value):
+ action = {"type": value, "duration": 0}
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": action_type,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", ["", "pauses"])
+async def test_params_input_source_action_sequence_actions_subtype_invalid_value(
+ perform_actions, action_type, value):
+ action = {"type": value, "duration": 0}
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": action_type,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_input_source_action_sequence_actions_pause_duration_invalid_type(
+ perform_actions, action_type, value):
+ action = {"type": "pause", "duration": value}
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": action_type,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+async def test_params_input_source_action_sequence_actions_pause_duration_invalid_value(
+ perform_actions, action_type, value):
+ action = {"type": "pause", "duration": value}
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": action_type,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("value", ["", "pauses"])
+async def test_params_null_action_type_invalid_value(perform_actions, value):
+ action = {"type": value, "duration": 0}
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "none",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+async def test_params_key_action_subtype_missing(perform_actions):
+ action = create_key_action("keyDown", {"value": "f"}, removals=["type"])
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "key",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("value", ["", "keyDowns", "keyUps"])
+async def test_params_key_action_subtype_invalid_value(perform_actions, value):
+ action = create_key_action(value, {"value": "f"})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "key",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("key_action", ["keyDown", "keyUp"])
+async def test_params_key_action_value_missing(perform_actions, key_action):
+ action = create_key_action(key_action, {"value": "f"}, removals=["value"])
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "key",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("key_action", ["keyDown", "keyUp"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_key_action_value_invalid_type(perform_actions,
+ key_action, value):
+ action = create_key_action(key_action, {"value": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "key",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize(
+ "value",
+ ["fa", "\u0BA8\u0BBFb", "\u0BA8\u0BBF\u0BA8", "\u1100\u1161\u11A8c"],
+)
+async def test_params_key_action_value_invalid_multiple_codepoints(
+ perform_actions, value):
+ actions = [
+ create_key_action("keyDown", {"value": value}),
+ create_key_action("keyUp", {"value": value}),
+ ]
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "key",
+ "id": "foo",
+ "actions": actions
+ }])
+
+
+@pytest.mark.parametrize("value",
+ ["", "pointerDowns", "pointerMoves", "pointerUps"])
+async def test_params_pointer_action_subtype_invalid_value(
+ perform_actions, value):
+ if value == "pointerMoves":
+ action = create_pointer_action(value, {"x": 0, "y": 0})
+ else:
+ action = create_pointer_action(value, {"button": 0})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+async def test_params_pointer_action_move_coordinate_missing(
+ perform_actions, coordinate):
+ action = create_pointer_action("pointerMove", removals=[coordinate])
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_move_coordinate_invalid_type(
+ perform_actions, coordinate, value):
+ action = create_pointer_action(
+ "pointerMove",
+ {
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+async def test_params_pointer_action_move_coordinate_invalid_value(
+ perform_actions, coordinate, value):
+ action = create_pointer_action(
+ "pointerMove",
+ {
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_pointer_action_move_origin_invalid_type(
+ perform_actions, value):
+ action = create_pointer_action("pointerMove", {"origin": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("value", ["", "pointers", "viewports"])
+async def test_params_pointer_action_move_origin_invalid_value(
+ perform_actions, value):
+ action = create_pointer_action("pointerMove", {"origin": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+async def test_params_pointer_action_up_down_button_missing(
+ perform_actions, pointer_action):
+ action = create_pointer_action(pointer_action, removals=["button"])
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_up_down_button_invalid_type(
+ perform_actions, pointer_action, value):
+ action = create_pointer_action(pointer_action, {"button": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+async def test_params_pointer_action_up_down_button_invalid_value(
+ perform_actions, pointer_action, value):
+ action = create_pointer_action(pointer_action, {"button": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action",
+ ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("dimension", ["width", "height"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_common_properties_dimensions_invalid_type(
+ perform_actions, dimension, pointer_action, value):
+ action = create_pointer_action(
+ pointer_action,
+ {
+ "width": value if dimension == "width" else 0,
+ "height": value if dimension == "height" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("dimension", ["width", "height"])
+@pytest.mark.parametrize("pointer_action",
+ ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+async def test_params_pointer_action_common_properties_dimensions_invalid_value(
+ perform_actions, dimension, pointer_action, value):
+ action = create_pointer_action(
+ pointer_action,
+ {
+ "width": value if dimension == "width" else 0,
+ "height": value if dimension == "height" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action",
+ ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("pressure", ["pressure", "tangentialPressure"])
+@pytest.mark.parametrize("value", [None, "foo", True, [], {}])
+async def test_params_pointer_action_common_properties_pressure_invalid_type(
+ perform_actions, pointer_action, pressure, value):
+ action = create_pointer_action(
+ pointer_action,
+ {
+ "pressure":
+ value if pressure == "pressure" else 0.0,
+ "tangentialPressure":
+ value if pressure == "tangentialPressure" else 0.0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action",
+ ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_common_properties_twist_invalid_type(
+ perform_actions, pointer_action, value):
+ action = create_pointer_action(pointer_action, {"twist": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action",
+ ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, 360])
+async def test_params_pointer_action_common_properties_twist_invalid_value(
+ perform_actions, pointer_action, value):
+ action = create_pointer_action(pointer_action, {"twist": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("pointer_action",
+ ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("angle", ["altitudeAngle", "azimuthAngle"])
+@pytest.mark.parametrize("value", [None, "foo", True, [], {}])
+async def test_params_pointer_action_common_properties_angle_invalid_type(
+ perform_actions, pointer_action, angle, value):
+ action = create_pointer_action(
+ pointer_action,
+ {
+ "altitudeAngle": value if angle == "altitudeAngle" else 0.0,
+ "azimuthAngle": value if angle == "azimuthAngle" else 0.0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "pointer",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_wheel_action_scroll_coordinate_invalid_type(
+ perform_actions, coordinate, value):
+ action = create_wheel_action(
+ "scroll",
+ {
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+async def test_params_wheel_action_scroll_coordinate_invalid_value(
+ perform_actions, coordinate, value):
+ action = create_wheel_action(
+ "scroll",
+ {
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("delta", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_wheel_action_scroll_delta_invalid_type(
+ perform_actions, delta, value):
+ action = create_wheel_action(
+ "scroll",
+ {
+ "deltaX": value if delta == "x" else 0,
+ "deltaY": value if delta == "y" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("delta", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+async def test_params_wheel_action_scroll_delta_invalid_value(
+ perform_actions, delta, value):
+ action = create_wheel_action(
+ "scroll",
+ {
+ "deltaX": value if delta == "x" else 0,
+ "deltaY": value if delta == "y" else 0,
+ },
+ )
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_wheel_action_scroll_origin_invalid_type(
+ perform_actions, value):
+ action = create_wheel_action("scroll", {"origin": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("value", ["", "pointers", "viewports"])
+async def test_params_wheel_action_scroll_origin_invalid_value(
+ perform_actions, value):
+ action = create_wheel_action("scroll", {"origin": value})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+async def test_params_wheel_action_scroll_origin_pointer_not_supported(
+ perform_actions):
+ # Pointer origin isn't currently supported for wheel input source
+ # See: https://github.com/w3c/webdriver/issues/1758
+ action = create_wheel_action("scroll", {"origin": "pointer"})
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("missing", ["x", "y", "deltaX", "deltaY"])
+async def test_params_wheel_action_scroll_property_missing(
+ perform_actions, missing):
+ action = create_wheel_action("scroll", removals=[missing])
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": "wheel",
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+# Element origin tests for pointer and wheel input sources
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+async def test_params_origin_element_type_invalid_type(perform_actions,
+ input_source, value):
+ origin = {"origin": {"type": value}}
+
+ if input_source == "pointer":
+ action = create_pointer_action("pointerMove", origin)
+ elif input_source == "wheel":
+ action = create_wheel_action("scroll", origin)
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": input_source,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+async def test_params_origin_element_element_missing(perform_actions,
+ input_source):
+ origin = {"origin": {"type": "element"}}
+
+ if input_source == "pointer":
+ action = create_pointer_action("pointerMove", origin)
+ elif input_source == "wheel":
+ action = create_wheel_action("scroll", origin)
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": input_source,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_origin_element_element_invalid_type(
+ perform_actions, input_source, value):
+ origin = {"origin": {"type": "element", "element": value}}
+
+ if input_source == "pointer":
+ action = create_pointer_action("pointerMove", origin)
+ elif input_source == "wheel":
+ action = create_wheel_action("scroll", origin)
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": input_source,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+async def test_params_origin_element_element_sharedid_missing(
+ perform_actions, input_source):
+ origin = {"origin": {"type": "element", "element": {}}}
+
+ if input_source == "pointer":
+ action = create_pointer_action("pointerMove", origin)
+ elif input_source == "wheel":
+ action = create_wheel_action("scroll", origin)
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": input_source,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+async def test_params_origin_element_element_sharedid_invalid_type(
+ perform_actions, input_source, value):
+ origin = {"origin": {"type": "element", "element": {"sharedId": value}}}
+
+ if input_source == "pointer":
+ action = create_pointer_action("pointerMove", origin)
+ elif input_source == "wheel":
+ action = create_wheel_action("scroll", origin)
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": input_source,
+ "id": "foo",
+ "actions": [action]
+ }])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+async def test_params_origin_element_invalid_with_shared_reference(
+ bidi_session, top_context, get_actions_origin_page, get_element,
+ perform_actions, input_source):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_actions_origin_page(""),
+ wait="complete",
+ )
+
+ origin = {"origin": await get_element("#inner")}
+
+ if input_source == "pointer":
+ action = create_pointer_action("pointerMove", origin)
+ elif input_source == "wheel":
+ action = create_wheel_action("scroll", origin)
+
+ with pytest.raises(InvalidArgumentException):
+ await perform_actions([{
+ "type": input_source,
+ "id": "foo",
+ "actions": [action]
+ }])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key.py
new file mode 100644
index 0000000000..9a04a1f31d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key.py
@@ -0,0 +1,107 @@
+import pytest
+
+from webdriver.bidi.error import NoSuchFrameException
+from webdriver.bidi.modules.input import Actions
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.keys import Keys
+from .. import get_keys_value
+from . import get_shadow_root_from_test_page
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_invalid_browsing_context(bidi_session):
+ actions = Actions()
+ actions.add_key()
+
+ with pytest.raises(NoSuchFrameException):
+ await bidi_session.input.perform_actions(actions=actions, context="foo")
+
+
+async def test_key_backspace(bidi_session, top_context, setup_key_test):
+ actions = Actions()
+ actions.add_key().send_keys("efcd").send_keys([Keys.BACKSPACE, Keys.BACKSPACE])
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == "ef"
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ ("\U0001F604"),
+ ("\U0001F60D"),
+ ("\u0BA8\u0BBF"),
+ ("\u1100\u1161\u11A8"),
+ ],
+)
+async def test_key_codepoint(
+ bidi_session, top_context, setup_key_test, value
+):
+ # Not using send_keys() because we always want to treat value as
+ # one character here. `len(value)` varies by platform for non-BMP characters,
+ # so we don't want to iterate over value.
+
+ actions = Actions()
+ (actions.add_key().key_down(value).key_up(value))
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ # events sent by major browsers are inconsistent so only check key value
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == value
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+async def test_key_shadow_tree(bidi_session, top_context, get_test_page, mode, nested):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(
+ shadow_doc="<div><input type=text></div>",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ ),
+ wait="complete",
+ )
+
+ shadow_root = await get_shadow_root_from_test_page(bidi_session, top_context, nested)
+ input_el = await bidi_session.script.call_function(
+ function_declaration="""shadowRoot => {{
+ const input = shadowRoot.querySelector('input');
+ input.focus();
+ return input;
+ }}
+ """,
+ arguments=[shadow_root],
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ actions = Actions()
+ (actions.add_key().key_down("a").key_up("a"))
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ input_value = await bidi_session.script.call_function(
+ function_declaration="input => input.value",
+ arguments=[input_el],
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ assert input_value["value"] == "a"
+
+
+async def test_null_response_value(bidi_session, top_context):
+ actions = Actions()
+ actions.add_key().key_down("a").key_up("a")
+ value = await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ assert value == {}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py
new file mode 100644
index 0000000000..e93c132e0a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_events.py
@@ -0,0 +1,268 @@
+# META: timeout=long
+import copy
+import pytest
+
+from collections import defaultdict
+
+from webdriver.bidi.modules.input import Actions
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.helpers import filter_dict, filter_supported_key_events
+from tests.support.keys import ALL_EVENTS, Keys, ALTERNATIVE_KEY_NAMES
+from .. import get_events, get_keys_value
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "key,event",
+ [
+ (Keys.ESCAPE, "ESCAPE"),
+ (Keys.RIGHT, "RIGHT"),
+ ],
+
+)
+async def test_non_printable_key_sends_events(
+ bidi_session, top_context, key, event
+):
+ code = ALL_EVENTS[event]["code"]
+ value = ALL_EVENTS[event]["key"]
+
+ actions = Actions()
+ (actions.add_key().key_down(key).key_up(key))
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ all_events = await get_events(bidi_session, top_context["context"])
+
+ expected = [
+ {"code": code, "key": value, "type": "keydown"},
+ {"code": code, "key": value, "type": "keypress"},
+ {"code": code, "key": value, "type": "keyup"},
+ ]
+
+ # Make a copy for alternate key property values
+ # Note: only keydown and keyup are affected by alternate key names
+ alt_expected = copy.deepcopy(expected)
+ if event in ALTERNATIVE_KEY_NAMES:
+ alt_expected[0]["key"] = ALTERNATIVE_KEY_NAMES[event]
+ alt_expected[2]["key"] = ALTERNATIVE_KEY_NAMES[event]
+
+ (_, expected) = filter_supported_key_events(all_events, expected)
+ (events, alt_expected) = filter_supported_key_events(all_events, alt_expected)
+ if len(events) == 2:
+ # most browsers don't send a keypress for non-printable keys
+ assert events == [expected[0], expected[2]] or events == [
+ alt_expected[0],
+ alt_expected[2],
+ ]
+ else:
+ assert events == expected or events == alt_expected
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert len(keys_value) == 0
+
+
+@pytest.mark.parametrize(
+ "key, event",
+ [
+ (Keys.ALT, "ALT"),
+ (Keys.CONTROL, "CONTROL"),
+ (Keys.META, "META"),
+ (Keys.SHIFT, "SHIFT"),
+ (Keys.R_ALT, "R_ALT"),
+ (Keys.R_CONTROL, "R_CONTROL"),
+ (Keys.R_META, "R_META"),
+ (Keys.R_SHIFT, "R_SHIFT"),
+ ],
+)
+async def test_key_modifier_key(bidi_session, top_context, setup_key_test, key, event):
+ code = ALL_EVENTS[event]["code"]
+ value = ALL_EVENTS[event]["key"]
+
+ actions = Actions()
+ (actions.add_key().key_down(key).key_up(key))
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ all_events = await get_events(bidi_session, top_context["context"])
+
+ expected = [
+ {"code": code, "key": value, "type": "keydown"},
+ {"code": code, "key": value, "type": "keyup"},
+ ]
+
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert len(keys_value) == 0
+
+
+@pytest.mark.parametrize(
+ "value,code",
+ [
+ ("a", "KeyA"),
+ ("a", "KeyA"),
+ ('"', "Quote"),
+ (",", "Comma"),
+ ("\u00E0", ""),
+ ("\u0416", ""),
+ ("@", "Digit2"),
+ ("\u2603", ""),
+ ("\uF6C2", ""), # PUA
+ ],
+)
+async def test_key_printable_key(
+ bidi_session,
+ top_context,
+ setup_key_test,
+ value,
+ code,
+):
+ actions = Actions()
+ (actions.add_key().key_down(value).key_up(value))
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ all_events = await get_events(bidi_session, top_context["context"])
+
+ expected = [
+ {"code": code, "key": value, "type": "keydown"},
+ {"code": code, "key": value, "type": "keypress"},
+ {"code": code, "key": value, "type": "keyup"},
+ ]
+
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == value
+
+
+@pytest.mark.parametrize("use_keyup", [True, False])
+async def test_key_printable_sequence(bidi_session, top_context, use_keyup):
+ actions = Actions()
+ actions.add_key()
+ if use_keyup:
+ actions.add_key().send_keys("ab")
+ else:
+ actions.add_key().key_down("a").key_down("b")
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ all_events = await get_events(bidi_session, top_context["context"])
+
+ expected = [
+ {"code": "KeyA", "key": "a", "type": "keydown"},
+ {"code": "KeyA", "key": "a", "type": "keypress"},
+ {"code": "KeyA", "key": "a", "type": "keyup"},
+ {"code": "KeyB", "key": "b", "type": "keydown"},
+ {"code": "KeyB", "key": "b", "type": "keypress"},
+ {"code": "KeyB", "key": "b", "type": "keyup"},
+ ]
+ expected = [e for e in expected if use_keyup or e["type"] != "keyup"]
+
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == "ab"
+
+
+@pytest.mark.parametrize("name,expected", ALL_EVENTS.items())
+async def test_key_special_key_sends_keydown(
+ bidi_session,
+ top_context,
+ setup_key_test,
+ name,
+ expected,
+):
+ if name.startswith("F"):
+ # Prevent default behavior for F1, etc., but only after keydown
+ # bubbles up to body. (Otherwise activated browser menus/functions
+ # may interfere with subsequent tests.)
+ await bidi_session.script.evaluate(
+ expression="""
+ document.body.addEventListener("keydown",
+ function(e) { e.preventDefault() });
+ """,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ actions = Actions()
+ (actions.add_key().key_down(getattr(Keys, name)))
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ # only interested in keydown
+ all_events = await get_events(bidi_session, top_context["context"])
+ first_event = all_events[0]
+ # make a copy so we can throw out irrelevant keys and compare to events
+ expected = dict(expected)
+
+ del expected["value"]
+
+ # make another copy for alternative key names
+ alt_expected = copy.deepcopy(expected)
+ if name in ALTERNATIVE_KEY_NAMES:
+ alt_expected["key"] = ALTERNATIVE_KEY_NAMES[name]
+
+ # check and remove keys that aren't in expected
+ assert first_event["type"] == "keydown"
+ assert first_event["repeat"] is False
+ first_event = filter_dict(first_event, expected)
+ if first_event["code"] is None:
+ del first_event["code"]
+ del expected["code"]
+ del alt_expected["code"]
+ assert first_event == expected or first_event == alt_expected
+ # only printable characters should be recorded in input field
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ if len(expected["key"]) == 1:
+ assert keys_value == expected["key"]
+ else:
+ assert len(keys_value) == 0
+
+
+async def test_key_space(bidi_session, top_context):
+ actions = Actions()
+ (
+ actions.add_key()
+ .key_down(Keys.SPACE)
+ .key_up(Keys.SPACE)
+ .key_down(" ")
+ .key_up(" ")
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ all_events = await get_events(bidi_session, top_context["context"])
+
+ by_type = defaultdict(list)
+ for event in all_events:
+ by_type[event["type"]].append(event)
+
+ for event_type in by_type:
+ events = by_type[event_type]
+ assert len(events) == 2
+ assert events[0] == events[1]
+
+
+async def test_keyup_only_sends_no_events(bidi_session, top_context):
+ actions = Actions()
+ actions.add_key().key_up("a")
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 0
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert len(keys_value) == 0
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_modifier.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_modifier.py
new file mode 100644
index 0000000000..e319bb70aa
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/key_modifier.py
@@ -0,0 +1,163 @@
+import pytest
+
+from webdriver.bidi.modules.input import Actions
+
+from tests.support.keys import Keys
+from .. import get_keys_value
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_meta_or_ctrl_with_printable_and_backspace_deletes_all_text(
+ bidi_session, top_context, setup_key_test, modifier_key
+):
+ actions = Actions()
+ (
+ actions.add_key()
+ .send_keys("abc d")
+ .key_down(modifier_key)
+ .key_down("a")
+ .key_up(modifier_key)
+ .key_up("a")
+ .key_down(Keys.BACKSPACE)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == ""
+
+
+async def test_meta_or_ctrl_with_printable_cut_and_paste_text(
+ bidi_session, top_context, setup_key_test, modifier_key
+):
+ initial = "abc d"
+ actions = Actions()
+ (
+ actions.add_key()
+ .send_keys(initial)
+ .key_down(modifier_key)
+ .key_down("a")
+ .key_up(modifier_key)
+ .key_up("a")
+ .key_down(modifier_key)
+ .key_down("x")
+ .key_up(modifier_key)
+ .key_up("x")
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == ""
+
+ actions = Actions()
+ (
+ actions.add_key()
+ .key_down(modifier_key)
+ .key_down("v")
+ .key_up(modifier_key)
+ .key_up("v")
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == initial
+
+
+async def test_meta_or_ctrl_with_printable_copy_and_paste_text(
+ bidi_session, top_context, setup_key_test, modifier_key
+):
+ initial = "abc d"
+ actions = Actions()
+ (
+ actions.add_key()
+ .send_keys(initial)
+ .key_down(modifier_key)
+ .key_down("a")
+ .key_up(modifier_key)
+ .key_up("a")
+ .key_down(modifier_key)
+ .key_down("c")
+ .key_up(modifier_key)
+ .key_up("c")
+ .send_keys([Keys.RIGHT])
+ .key_down(modifier_key)
+ .key_down("v")
+ .key_up(modifier_key)
+ .key_up("v")
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+ assert keys_value == initial * 2
+
+
+@pytest.mark.parametrize("modifier", [Keys.SHIFT, Keys.R_SHIFT])
+async def test_key_modifier_shift_non_printable_keys(
+ bidi_session, top_context, setup_key_test, modifier
+):
+ actions = Actions()
+ (
+ actions.add_key()
+ .key_down("f")
+ .key_up("f")
+ .key_down("o")
+ .key_up("o")
+ .key_down("o")
+ .key_up("o")
+ .key_down(modifier)
+ .key_down(Keys.BACKSPACE)
+ .key_up(modifier)
+ .key_up(Keys.BACKSPACE)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+
+ assert keys_value == "fo"
+
+
+@pytest.mark.parametrize("modifier", [Keys.SHIFT, Keys.R_SHIFT])
+async def test_key_modifier_shift_printable_keys(
+ bidi_session, top_context, setup_key_test, modifier
+):
+ actions = Actions()
+ (
+ actions.add_key()
+ .key_down("b")
+ .key_up("b")
+ .key_down(modifier)
+ .key_down("c")
+ .key_up(modifier)
+ .key_up("c")
+ .key_down("d")
+ .key_up("d")
+ .key_down(modifier)
+ .key_down("e")
+ .key_up("e")
+ .key_down("f")
+ .key_up(modifier)
+ .key_up("f")
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ keys_value = await get_keys_value(bidi_session, top_context["context"])
+
+ assert keys_value == "bCdEF"
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer.py
new file mode 100644
index 0000000000..6109450a23
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer.py
@@ -0,0 +1,15 @@
+import pytest
+
+from webdriver.bidi.error import NoSuchFrameException
+from webdriver.bidi.modules.input import Actions
+
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_invalid_browsing_context(bidi_session):
+ actions = Actions()
+ actions.add_pointer()
+
+ with pytest.raises(NoSuchFrameException):
+ await bidi_session.input.perform_actions(actions=actions, context="foo")
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py
new file mode 100644
index 0000000000..7077d7bba4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py
@@ -0,0 +1,318 @@
+import pytest
+
+from webdriver.bidi.error import MoveTargetOutOfBoundsException
+from webdriver.bidi.modules.input import Actions, get_element_origin
+
+from tests.support.asserts import assert_move_to_coordinates
+from tests.support.helpers import filter_dict
+
+from .. import get_events
+from . import (
+ assert_pointer_events,
+ get_inview_center_bidi,
+ get_shadow_root_from_test_page,
+ record_pointer_events,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_click_at_coordinates(bidi_session, top_context, load_static_test_page):
+ await load_static_test_page(page="test_actions.html")
+
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=div_point["x"], y=div_point["y"], duration=1000)
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+
+ assert len(events) == 4
+ assert_move_to_coordinates(div_point, "outer", events)
+
+ for e in events:
+ if e["type"] != "mousedown":
+ assert e["buttons"] == 0
+ assert e["button"] == 0
+
+ expected = [
+ {"type": "mousedown", "buttons": 1},
+ {"type": "mouseup", "buttons": 0},
+ {"type": "click", "buttons": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
+
+
+@pytest.mark.parametrize("origin", ["pointer", "viewport"])
+async def test_params_actions_origin_outside_viewport(bidi_session, top_context, origin):
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=-50, y=-50, origin=origin)
+
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+
+async def test_params_actions_origin_element_outside_viewport(
+ bidi_session, top_context, get_actions_origin_page, get_element
+):
+ url = get_actions_origin_page(
+ """width: 100px; height: 50px; background: green;
+ position: relative; left: -200px; top: -100px;"""
+ )
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ elem = await get_element("#inner")
+
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=0, y=0, origin=get_element_origin(elem))
+
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+
+async def test_context_menu_at_coordinates(
+ bidi_session, top_context, load_static_test_page
+):
+ await load_static_test_page(page="test_actions.html")
+
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=div_point["x"], y=div_point["y"])
+ .pointer_down(button=2)
+ .pointer_up(button=2)
+ )
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 4
+
+ expected = [
+ {"type": "mousedown", "button": 2, "buttons": 2},
+ {"type": "contextmenu", "button": 2, "buttons": 2},
+ ]
+ # Some browsers in some platforms may dispatch `contextmenu` event as a
+ # a default action of `mouseup`. In the case, `.buttons` of the event
+ # should be 0.
+ anotherExpected = [
+ {"type": "mousedown", "button": 2, "buttons": 2},
+ {"type": "contextmenu", "button": 2, "buttons": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ mousedown_contextmenu_events = [
+ x for x in filtered_events if x["type"] in ["mousedown", "contextmenu"]
+ ]
+ assert mousedown_contextmenu_events in [expected, anotherExpected]
+
+
+async def test_middle_click(bidi_session, top_context, load_static_test_page):
+ await load_static_test_page(page="test_actions.html")
+
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=div_point["x"], y=div_point["y"])
+ .pointer_down(button=1)
+ .pointer_up(button=1)
+ )
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 3
+
+ expected = [
+ {"type": "mousedown", "button": 1, "buttons": 4},
+ {"type": "mouseup", "button": 1, "buttons": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ mousedown_mouseup_events = [
+ x for x in filtered_events if x["type"] in ["mousedown", "mouseup"]
+ ]
+ assert expected == mousedown_mouseup_events
+
+
+async def test_click_element_center(
+ bidi_session, top_context, get_element, load_static_test_page
+):
+ await load_static_test_page(page="test_actions.html")
+
+ outer = await get_element("#outer")
+ center = await get_inview_center_bidi(
+ bidi_session, context=top_context, element=outer
+ )
+
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(outer))
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 4
+
+ event_types = [e["type"] for e in events]
+ assert ["mousemove", "mousedown", "mouseup", "click"] == event_types
+ for e in events:
+ if e["type"] != "mousemove":
+ assert e["pageX"] == pytest.approx(center["x"], abs=1.0)
+ assert e["pageY"] == pytest.approx(center["y"], abs=1.0)
+ assert e["target"] == "outer"
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+async def test_click_element_in_shadow_tree(
+ bidi_session, top_context, get_test_page, mode, nested
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(
+ shadow_doc="""
+ <div id="pointer-target"
+ style="width: 10px; height: 10px; background-color:blue;">
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ ),
+ wait="complete",
+ )
+
+ shadow_root = await get_shadow_root_from_test_page(
+ bidi_session, top_context, nested
+ )
+
+ target = await record_pointer_events(
+ bidi_session, top_context, shadow_root, "#pointer-target"
+ )
+
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(target))
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ await assert_pointer_events(
+ bidi_session,
+ top_context,
+ expected_events=["pointerdown", "pointerup"],
+ target="pointer-target",
+ pointer_type="mouse",
+ )
+
+
+async def test_click_navigation(
+ bidi_session,
+ top_context,
+ url,
+ inline,
+ subscribe_events,
+ wait_for_event,
+ wait_for_future_safe,
+ get_element,
+):
+ await subscribe_events(events=["browsingContext.load"])
+
+ destination = url("/webdriver/tests/support/html/test_actions.html")
+ start = inline(f'<a href="{destination}" id="link">destination</a>')
+
+ async def click_link():
+ link = await get_element("#link")
+
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(link))
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ # repeat steps to check behaviour after document unload
+ for _ in range(2):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=start, wait="complete"
+ )
+
+ on_entry = wait_for_event("browsingContext.load")
+ await click_link()
+ event = await wait_for_future_safe(on_entry)
+ assert event["url"] == destination
+
+
+@pytest.mark.parametrize("x, y, event_count", [
+ (0, 0, 0),
+ (1, 0, 1),
+ (0, 1, 1),
+], ids=["default value", "x", "y"])
+async def test_move_to_position_in_viewport(
+ bidi_session, load_static_test_page, top_context, x, y, event_count
+):
+ await load_static_test_page(page="test_actions.html")
+
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=x, y=y)
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == event_count
+
+ # Move again to check that no further mouse move event is emitted.
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=x, y=y)
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == event_count
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_drag.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_drag.py
new file mode 100644
index 0000000000..7cd2e386c6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_drag.py
@@ -0,0 +1,137 @@
+# META: timeout=long
+
+import pytest
+
+from webdriver.bidi.modules.input import Actions, get_element_origin
+
+from .. import get_events
+from . import get_element_rect, get_inview_center_bidi
+
+pytestmark = pytest.mark.asyncio
+
+@pytest.mark.parametrize("drag_duration", [0, 300, 800])
+@pytest.mark.parametrize(
+ "dx, dy", [(20, 0), (0, 15), (10, 15), (-20, 0), (10, -15), (-10, -15)]
+)
+async def test_drag_and_drop(
+ bidi_session,
+ top_context,
+ get_element,
+ load_static_test_page,
+ dx,
+ dy,
+ drag_duration,
+):
+ await load_static_test_page(page="test_actions.html")
+
+ drag_target = await get_element("#dragTarget")
+ initial_rect = await get_element_rect(
+ bidi_session, context=top_context, element=drag_target
+ )
+ initial_center = await get_inview_center_bidi(
+ bidi_session, context=top_context, element=drag_target
+ )
+
+ # Conclude chain with extra move to allow time for last queued
+ # coordinate-update of drag_target and to test that drag_target is "dropped".
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(drag_target))
+ .pointer_down(button=0)
+ .pointer_move(dx, dy, duration=drag_duration, origin="pointer")
+ .pointer_up(button=0)
+ .pointer_move(80, 50, duration=100, origin="pointer")
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ # mouseup that ends the drag is at the expected destination
+ events = await get_events(bidi_session, top_context["context"])
+ e = events[1]
+ assert e["type"] == "mouseup"
+ assert e["pageX"] == pytest.approx(initial_center["x"] + dx, abs=1.0)
+ assert e["pageY"] == pytest.approx(initial_center["y"] + dy, abs=1.0)
+ # check resulting location of the dragged element
+ final_rect = await get_element_rect(
+ bidi_session, context=top_context, element=drag_target
+ )
+ assert initial_rect["x"] + dx == final_rect["x"]
+ assert initial_rect["y"] + dy == final_rect["y"]
+
+
+@pytest.mark.parametrize("drag_duration", [0, 300, 800])
+async def test_drag_and_drop_with_draggable_element(bidi_session, top_context,
+ get_element,
+ load_static_test_page,
+ drag_duration):
+ await load_static_test_page(page="test_actions.html")
+
+ drag_target = await get_element("#draggable")
+ drop_target = await get_element("#droppable")
+
+ # Conclude chain with extra move to allow time for last queued
+ # coordinate-update of drag_target and to test that drag_target is "dropped".
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(drag_target))
+ .pointer_down(button=0)
+ .pointer_move(x=0, y=0, duration=drag_duration, origin=get_element_origin(drop_target))
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(actions=actions,
+ context=top_context["context"])
+
+ # mouseup that ends the drag is at the expected destination
+ events = await get_events(bidi_session, top_context["context"])
+
+ drag_events_captured = [
+ ev["type"] for ev in events
+ if ev["type"].startswith("drag") or ev["type"].startswith("drop")
+ ]
+ assert "dragstart" in drag_events_captured
+ assert "dragenter" in drag_events_captured
+ # dragleave never happens if the mouse moves directly into the drop element
+ # without intermediate movements.
+ if drag_duration != 0:
+ assert "dragleave" in drag_events_captured
+ assert "dragover" in drag_events_captured
+ assert "drop" in drag_events_captured
+ assert "dragend" in drag_events_captured
+
+ def last_index(list, value):
+ return len(list) - list[::-1].index(value) - 1
+
+ # The order should follow the diagram:
+ #
+ # - dragstart
+ # - dragenter
+ # - ...
+ # - dragenter
+ # - dragleave
+ # - ...
+ # - dragleave
+ # - dragover
+ # - ...
+ # - dragover
+ # - drop
+ # - dragend
+ #
+ assert drag_events_captured.index(
+ "dragstart") < drag_events_captured.index("dragenter")
+ if drag_duration != 0:
+ assert last_index(drag_events_captured,
+ "dragenter") < last_index(drag_events_captured, "dragleave")
+ assert last_index(drag_events_captured,
+ "dragleave") < last_index(drag_events_captured, "dragover")
+ else:
+ assert last_index(drag_events_captured,
+ "dragenter") < last_index(drag_events_captured, "dragover")
+ assert last_index(drag_events_captured,
+ "dragover") < drag_events_captured.index("drop")
+ assert drag_events_captured.index(
+ "drop") == drag_events_captured.index("dragend") - 1
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_modifier.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_modifier.py
new file mode 100644
index 0000000000..a2d0ae7075
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_modifier.py
@@ -0,0 +1,242 @@
+import pytest
+
+from webdriver.bidi.modules.input import Actions, get_element_origin
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.helpers import filter_dict
+from tests.support.keys import Keys
+
+from .. import get_events
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "modifier, prop",
+ [
+ (Keys.CONTROL, "ctrlKey"),
+ (Keys.R_CONTROL, "ctrlKey"),
+ ],
+)
+async def test_control_click(
+ bidi_session,
+ current_session,
+ top_context,
+ get_element,
+ load_static_test_page,
+ modifier,
+ prop,
+):
+ os = current_session.capabilities["platformName"]
+
+ await load_static_test_page(page="test_actions.html")
+ outer = await get_element("#outer")
+
+ actions = Actions()
+ (
+ actions.add_key()
+ .pause(duration=0)
+ .key_down(modifier)
+ .pause(duration=200)
+ .key_up(modifier)
+ )
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(outer))
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ if os != "mac":
+ expected = [
+ {"type": "mousemove"},
+ {"type": "mousedown"},
+ {"type": "mouseup"},
+ {"type": "click"},
+ ]
+ else:
+ expected = [
+ {"type": "mousemove"},
+ {"type": "mousedown"},
+ {"type": "contextmenu"},
+ {"type": "mouseup"},
+ ]
+
+ defaults = {"altKey": False, "metaKey": False, "shiftKey": False, "ctrlKey": False}
+
+ for e in expected:
+ e.update(defaults)
+ if e["type"] != "mousemove":
+ e[prop] = True
+
+ all_events = await get_events(bidi_session, top_context["context"])
+ filtered_events = [filter_dict(e, expected[0]) for e in all_events]
+ assert expected == filtered_events
+
+
+async def test_control_click_release(
+ bidi_session, top_context, load_static_test_page, get_focused_key_input
+):
+ await load_static_test_page(page="test_actions.html")
+ key_reporter = await get_focused_key_input()
+
+ # The context menu stays visible during subsequent tests so let's not
+ # display it in the first place.
+ await bidi_session.script.evaluate(
+ expression="""
+ var keyReporter = document.getElementById("keys");
+ document.addEventListener("contextmenu", function(e) {
+ e.preventDefault();
+ });
+ """,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ actions = Actions()
+ actions.add_key().pause(duration=0).key_down(Keys.CONTROL)
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(key_reporter))
+ .pointer_down(button=0)
+ )
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ await bidi_session.script.evaluate(
+ expression="""
+ var keyReporter = document.getElementById("keys");
+ keyReporter.addEventListener("mousedown", recordPointerEvent);
+ keyReporter.addEventListener("mouseup", recordPointerEvent);
+ resetEvents();
+ """,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ await bidi_session.input.release_actions(context=top_context["context"])
+
+ expected = [
+ {"type": "mouseup"},
+ {"type": "keyup"},
+ ]
+ all_events = await get_events(bidi_session, top_context["context"])
+ events = [filter_dict(e, expected[0]) for e in all_events]
+ assert events == expected
+
+
+async def test_many_modifiers_click(
+ bidi_session, top_context, get_element, load_static_test_page
+):
+ await load_static_test_page(page="test_actions.html")
+ outer = await get_element("#outer")
+
+ dblclick_timeout = 800
+ actions = Actions()
+ (
+ actions.add_key()
+ .pause(duration=0)
+ .key_down(Keys.ALT)
+ .key_down(Keys.SHIFT)
+ .pause(duration=dblclick_timeout)
+ .key_up(Keys.ALT)
+ .key_up(Keys.SHIFT)
+ )
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(outer))
+ .pause(duration=0)
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ .pause(duration=0)
+ .pause(duration=0)
+ .pointer_down(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ expected = [
+ {"type": "mousemove"},
+ # shift and alt pressed
+ {"type": "mousedown"},
+ {"type": "mouseup"},
+ {"type": "click"},
+ # no modifiers pressed
+ {"type": "mousedown"},
+ ]
+
+ defaults = {"altKey": False, "metaKey": False, "shiftKey": False, "ctrlKey": False}
+
+ for e in expected:
+ e.update(defaults)
+
+ for e in expected[1:4]:
+ e["shiftKey"] = True
+ e["altKey"] = True
+
+ all_events = await get_events(bidi_session, top_context["context"])
+ events = [filter_dict(e, expected[0]) for e in all_events]
+ assert events == expected
+
+
+@pytest.mark.parametrize(
+ "modifier, prop",
+ [
+ (Keys.ALT, "altKey"),
+ (Keys.R_ALT, "altKey"),
+ (Keys.META, "metaKey"),
+ (Keys.R_META, "metaKey"),
+ (Keys.SHIFT, "shiftKey"),
+ (Keys.R_SHIFT, "shiftKey"),
+ ],
+)
+async def test_modifier_click(
+ bidi_session, top_context, get_element, load_static_test_page, modifier, prop
+):
+ await load_static_test_page(page="test_actions.html")
+ outer = await get_element("#outer")
+
+ actions = Actions()
+ (
+ actions.add_key()
+ .pause(duration=200)
+ .key_down(modifier)
+ .pause(duration=200)
+ .pause(duration=0)
+ .key_up(modifier)
+ )
+ (
+ actions.add_pointer()
+ .pointer_move(x=0, y=0, origin=get_element_origin(outer))
+ .pause(duration=50)
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ expected = [
+ {"type": "mousemove"},
+ {"type": "mousedown"},
+ {"type": "mouseup"},
+ {"type": "click"},
+ ]
+
+ defaults = {"altKey": False, "metaKey": False, "shiftKey": False, "ctrlKey": False}
+
+ for e in expected:
+ e.update(defaults)
+ if e["type"] != "mousemove":
+ e[prop] = True
+
+ all_events = await get_events(bidi_session, top_context["context"])
+ filtered_events = [filter_dict(e, expected[0]) for e in all_events]
+ assert expected == filtered_events
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_multiclick.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_multiclick.py
new file mode 100644
index 0000000000..3538105341
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_multiclick.py
@@ -0,0 +1,162 @@
+import pytest
+
+from webdriver.bidi.modules.input import Actions
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.asserts import assert_move_to_coordinates
+from tests.support.helpers import filter_dict
+
+from .. import get_events
+from . import get_element_rect
+
+pytestmark = pytest.mark.asyncio
+
+
+_DBLCLICK_INTERVAL = 640
+
+
+@pytest.mark.parametrize("pause_during_click", [True, False])
+@pytest.mark.parametrize("click_pause", [0, 200, _DBLCLICK_INTERVAL + 10])
+async def test_dblclick_at_coordinates(
+ bidi_session, top_context, load_static_test_page, pause_during_click, click_pause
+):
+ await load_static_test_page(page="test_actions.html")
+
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ actions = Actions()
+ input_source = (
+ actions.add_pointer()
+ .pointer_move(x=div_point["x"], y=div_point["y"])
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ # Either pause before the second click, which might prevent the double click
+ # depending on the pause delay. Or between mousedown and mouseup for the
+ # second click, which will never prevent a double click.
+ if pause_during_click:
+ input_source.pointer_down(button=0).pause(duration=click_pause)
+ else:
+ input_source.pause(duration=click_pause).pointer_down(button=0)
+
+ input_source.pointer_up(button=0)
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ # mouseup that ends the drag is at the expected destination
+ events = await get_events(bidi_session, top_context["context"])
+
+ assert_move_to_coordinates(div_point, "outer", events)
+
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ ]
+
+ if pause_during_click or click_pause < _DBLCLICK_INTERVAL:
+ expected.append({"type": "dblclick", "button": 0})
+
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
+
+
+async def test_no_dblclick_when_mouse_moves(
+ bidi_session, top_context, load_static_test_page
+):
+ await load_static_test_page(page="test_actions.html")
+
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=div_point["x"], y=div_point["y"])
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ .pointer_move(x=div_point["x"] + 10, y=div_point["y"] + 10)
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ ]
+
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
+
+
+lots_of_text = (
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor "
+ "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud "
+ "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
+)
+
+
+async def test_tripleclick_at_coordinates(
+ bidi_session, top_context, inline, get_element
+):
+ """
+ This test does a triple click on a coordinate. On desktop platforms
+ this will select a paragraph. On mobile this will not have the same
+ desired outcome as taps are handled differently on mobile.
+ """
+ url = inline(
+ f"""<div>{lots_of_text}</div>"""
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ div = await get_element("div")
+ div_rect = await get_element_rect(bidi_session, context=top_context, element=div)
+ div_centre = {
+ "x": div_rect["x"] + div_rect["width"] / 2,
+ "y": div_rect["y"] + div_rect["height"] / 2,
+ }
+
+ actions = Actions()
+ (
+ actions.add_pointer()
+ .pointer_move(x=int(div_centre["x"]), y=int(div_centre["y"]))
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ actual_text = await bidi_session.script.evaluate(
+ expression="document.getSelection().toString()",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ assert actual_text["value"] == lots_of_text
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_origin.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_origin.py
new file mode 100644
index 0000000000..f6721e07f3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_origin.py
@@ -0,0 +1,196 @@
+import pytest
+
+from webdriver.bidi.error import NoSuchElementException
+from webdriver.bidi.modules.input import Actions, get_element_origin
+from webdriver.bidi.modules.script import ContextTarget
+
+from . import (
+ get_inview_center_bidi,
+ remote_mapping_to_dict,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+async def get_click_coordinates(bidi_session, context):
+ """Helper to get recorded click coordinates on a page generated with the
+ actions_origins_doc fixture."""
+ result = await bidi_session.script.evaluate(
+ expression="window.coords",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+ return remote_mapping_to_dict(result["value"])
+
+
+async def test_viewport_inside(bidi_session, top_context,
+ get_actions_origin_page):
+ point = {"x": 50, "y": 50}
+
+ url = get_actions_origin_page(
+ "width: 100px; height: 50px; background: green;")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=point["x"], y=point["y"])
+ await bidi_session.input.perform_actions(actions=actions,
+ context=top_context["context"])
+
+ click_coords = await get_click_coordinates(bidi_session,
+ context=top_context)
+ assert click_coords["x"] == pytest.approx(point["x"], abs=1.0)
+ assert click_coords["y"] == pytest.approx(point["y"], abs=1.0)
+
+
+async def test_pointer_inside(bidi_session, top_context,
+ get_actions_origin_page):
+ start_point = {"x": 50, "y": 50}
+ offset = {"x": 10, "y": 5}
+
+ url = get_actions_origin_page(
+ "width: 100px; height: 50px; background: green;")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ actions = Actions()
+ (actions.add_pointer().pointer_move(
+ x=start_point["x"], y=start_point["y"]).pointer_move(x=offset["x"],
+ y=offset["y"],
+ origin="pointer"))
+
+ await bidi_session.input.perform_actions(actions=actions,
+ context=top_context["context"])
+
+ click_coords = await get_click_coordinates(bidi_session,
+ context=top_context)
+ assert click_coords["x"] == pytest.approx(start_point["x"] + offset["x"],
+ abs=1.0)
+ assert click_coords["y"] == pytest.approx(start_point["y"] + offset["y"],
+ abs=1.0)
+
+
+@pytest.mark.parametrize(
+ "doc",
+ [
+ "width: 100px; height: 50px; background: green;",
+ """width: 100px; height: 50px; background: green;
+ position: relative; left: -50px; top: -25px;""",
+ ],
+ ids=["element fully visible", "element partly visible"],
+)
+@pytest.mark.parametrize("offset_x, offset_y", [(10, 15), (0, 0)])
+async def test_element_center_point_with_offset(
+ bidi_session,
+ top_context,
+ get_actions_origin_page,
+ get_element,
+ doc,
+ offset_x,
+ offset_y,
+):
+ url = get_actions_origin_page(doc)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ elem = await get_element("#inner")
+ center = await get_inview_center_bidi(bidi_session,
+ context=top_context,
+ element=elem)
+
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=offset_x,
+ y=offset_y,
+ origin=get_element_origin(elem))
+ await bidi_session.input.perform_actions(actions=actions,
+ context=top_context["context"])
+
+ click_coords = await get_click_coordinates(bidi_session,
+ context=top_context)
+ assert click_coords["x"] == pytest.approx(center["x"] + offset_x, abs=1.0)
+ assert click_coords["y"] == pytest.approx(center["y"] + offset_y, abs=1.0)
+
+
+async def test_element_larger_than_viewport(bidi_session, top_context,
+ get_actions_origin_page,
+ get_element):
+ url = get_actions_origin_page(
+ "width: 300vw; height: 300vh; background: green;")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ elem = await get_element("#inner")
+ center = await get_inview_center_bidi(bidi_session,
+ context=top_context,
+ element=elem)
+
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=0,
+ y=0,
+ origin=get_element_origin(elem))
+ await bidi_session.input.perform_actions(actions=actions,
+ context=top_context["context"])
+
+ click_coords = await get_click_coordinates(bidi_session,
+ context=top_context)
+ assert click_coords["x"] == pytest.approx(center["x"], abs=1.0)
+ assert click_coords["y"] == pytest.approx(center["y"], abs=1.0)
+
+
+@pytest.mark.parametrize(
+ "expression",
+ [
+ "document.querySelector('input#button').attributes[0]",
+ "document.querySelector('#with-text-node').childNodes[0]",
+ """document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")""",
+ "document.querySelector('#with-comment').childNodes[0]",
+ "document",
+ "document.doctype",
+ "document.createDocumentFragment()",
+ "document.querySelector('#custom-element').shadowRoot",
+ ],
+ ids=[
+ "attribute",
+ "text node",
+ "processing instruction",
+ "comment",
+ "document",
+ "doctype",
+ "document fragment",
+ "shadow root",
+ ]
+)
+async def test_params_actions_origin_no_such_element(
+ bidi_session, top_context, get_test_page, expression
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(),
+ wait="complete",
+ )
+
+ node = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ actions = Actions()
+ actions.add_pointer().pointer_move(x=0, y=0, origin=get_element_origin(node))
+
+ with pytest.raises(NoSuchElementException):
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_pen.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_pen.py
new file mode 100644
index 0000000000..def4552d30
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_pen.py
@@ -0,0 +1,126 @@
+import pytest
+
+from webdriver.bidi.modules.input import Actions, get_element_origin
+
+from .. import get_events
+from . import (
+ assert_pointer_events,
+ get_inview_center_bidi,
+ get_shadow_root_from_test_page,
+ record_pointer_events,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+async def test_pen_pointer_in_shadow_tree(
+ bidi_session, top_context, get_test_page, mode, nested
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(
+ shadow_doc="""
+ <div id="pointer-target"
+ style="width: 10px; height: 10px; background-color:blue;">
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ ),
+ wait="complete",
+ )
+
+ shadow_root = await get_shadow_root_from_test_page(
+ bidi_session, top_context, nested
+ )
+
+ # Add a simplified event recorder to track events in the test ShadowRoot.
+ target = await record_pointer_events(
+ bidi_session, top_context, shadow_root, "#pointer-target"
+ )
+
+ actions = Actions()
+ (
+ actions.add_pointer(pointer_type="pen")
+ .pointer_move(x=0, y=0, origin=get_element_origin(target))
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ await assert_pointer_events(
+ bidi_session,
+ top_context,
+ expected_events=["pointerdown", "pointerup"],
+ target="pointer-target",
+ pointer_type="pen",
+ )
+
+
+async def test_pen_pointer_properties(
+ bidi_session, top_context, get_element, load_static_test_page
+):
+ await load_static_test_page(page="test_actions_pointer.html")
+
+ pointerArea = await get_element("#pointerArea")
+ center = await get_inview_center_bidi(
+ bidi_session, context=top_context, element=pointerArea
+ )
+
+ actions = Actions()
+ (
+ actions.add_pointer(pointer_type="pen")
+ .pointer_move(x=0, y=0, origin=get_element_origin(pointerArea))
+ .pointer_down(button=0, pressure=0.36, altitude_angle=0.3, azimuth_angle=0.2419, twist=86)
+ .pointer_move(x=10, y=10, origin=get_element_origin(pointerArea))
+ .pointer_up(button=0)
+ .pointer_move(x=80, y=50, origin=get_element_origin(pointerArea))
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 10
+ event_types = [e["type"] for e in events]
+ assert [
+ "pointerover",
+ "pointerenter",
+ "pointermove",
+ "pointerdown",
+ "pointerover",
+ "pointerenter",
+ "pointermove",
+ "pointerup",
+ "pointerout",
+ "pointerleave",
+ ] == event_types
+ assert events[3]["type"] == "pointerdown"
+ assert events[3]["pageX"] == pytest.approx(center["x"], abs=1.0)
+ assert events[3]["pageY"] == pytest.approx(center["y"], abs=1.0)
+ assert events[3]["target"] == "pointerArea"
+ assert events[3]["pointerType"] == "pen"
+ # The default value of width and height for mouse and pen inputs is 1
+ assert round(events[3]["width"], 2) == 1
+ assert round(events[3]["height"], 2) == 1
+ assert round(events[3]["pressure"], 2) == 0.36
+ assert events[3]["tiltX"] == 72
+ assert events[3]["tiltY"] == 38
+ assert events[3]["twist"] == 86
+ assert events[6]["type"] == "pointermove"
+ assert events[6]["pageX"] == pytest.approx(center["x"] + 10, abs=1.0)
+ assert events[6]["pageY"] == pytest.approx(center["y"] + 10, abs=1.0)
+ assert events[6]["target"] == "pointerArea"
+ assert events[6]["pointerType"] == "pen"
+ assert round(events[6]["width"], 2) == 1
+ assert round(events[6]["height"], 2) == 1
+ # The default value of pressure for all inputs is 0.5, other properties are 0
+ assert round(events[6]["pressure"], 2) == 0.5
+ assert events[6]["tiltX"] == 0
+ assert events[6]["tiltY"] == 0
+ assert events[6]["twist"] == 0
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_touch.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_touch.py
new file mode 100644
index 0000000000..95aa62ceba
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_touch.py
@@ -0,0 +1,195 @@
+import pytest
+
+from webdriver.bidi.modules.input import Actions, get_element_origin
+
+from .. import get_events
+from . import (
+ assert_pointer_events,
+ get_inview_center_bidi,
+ get_shadow_root_from_test_page,
+ record_pointer_events,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+async def test_touch_pointer_in_shadow_tree(
+ bidi_session, top_context, get_test_page, mode, nested
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(
+ shadow_doc="""
+ <div id="pointer-target"
+ style="width: 10px; height: 10px; background-color:blue;">
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ ),
+ wait="complete",
+ )
+
+ shadow_root = await get_shadow_root_from_test_page(
+ bidi_session, top_context, nested
+ )
+
+ # Add a simplified event recorder to track events in the test ShadowRoot.
+ target = await record_pointer_events(
+ bidi_session, top_context, shadow_root, "#pointer-target"
+ )
+
+ actions = Actions()
+ (
+ actions.add_pointer(pointer_type="touch")
+ .pointer_move(x=0, y=0, origin=get_element_origin(target))
+ .pointer_down(button=0)
+ .pointer_up(button=0)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ await assert_pointer_events(
+ bidi_session,
+ top_context,
+ expected_events=["pointerdown", "pointerup"],
+ target="pointer-target",
+ pointer_type="touch",
+ )
+
+
+async def test_touch_pointer_properties(
+ bidi_session, top_context, get_element, load_static_test_page
+):
+ await load_static_test_page(page="test_actions_pointer.html")
+
+ pointerArea = await get_element("#pointerArea")
+ center = await get_inview_center_bidi(
+ bidi_session, context=top_context, element=pointerArea
+ )
+
+ actions = Actions()
+ (
+ actions.add_pointer(pointer_type="touch")
+ .pointer_move(x=0, y=0, origin=get_element_origin(pointerArea))
+ .pointer_down(
+ button=0,
+ width=23,
+ height=31,
+ pressure=0.78,
+ twist=355,
+ )
+ .pointer_move(
+ x=10,
+ y=10,
+ origin=get_element_origin(pointerArea),
+ width=39,
+ height=35,
+ pressure=0.91,
+ twist=345,
+ )
+ .pointer_up(button=0)
+ .pointer_move(x=80, y=50, origin=get_element_origin(pointerArea))
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+
+ assert len(events) == 7
+ event_types = [e["type"] for e in events]
+ assert [
+ "pointerover",
+ "pointerenter",
+ "pointerdown",
+ "pointermove",
+ "pointerup",
+ "pointerout",
+ "pointerleave",
+ ] == event_types
+ assert events[2]["type"] == "pointerdown"
+ assert events[2]["pageX"] == pytest.approx(center["x"], abs=1.0)
+ assert events[2]["pageY"] == pytest.approx(center["y"], abs=1.0)
+ assert events[2]["target"] == "pointerArea"
+ assert events[2]["pointerType"] == "touch"
+ assert round(events[2]["width"], 2) == 23
+ assert round(events[2]["height"], 2) == 31
+ assert round(events[2]["pressure"], 2) == 0.78
+ assert events[3]["type"] == "pointermove"
+ assert events[3]["pageX"] == pytest.approx(center["x"] + 10, abs=1.0)
+ assert events[3]["pageY"] == pytest.approx(center["y"] + 10, abs=1.0)
+ assert events[3]["target"] == "pointerArea"
+ assert events[3]["pointerType"] == "touch"
+ assert round(events[3]["width"], 2) == 39
+ assert round(events[3]["height"], 2) == 35
+ assert round(events[3]["pressure"], 2) == 0.91
+
+
+async def test_touch_pointer_properties_angle_twist(
+ bidi_session, top_context, get_element, load_static_test_page
+):
+ await load_static_test_page(page="test_actions_pointer.html")
+
+ pointerArea = await get_element("#pointerArea")
+ await get_inview_center_bidi(
+ bidi_session, context=top_context, element=pointerArea
+ )
+
+ actions = Actions()
+ (
+ actions.add_pointer(pointer_type="touch")
+ .pointer_move(x=0, y=0, origin=get_element_origin(pointerArea))
+ .pointer_down(
+ button=0,
+ width=23,
+ height=31,
+ pressure=0.78,
+ altitude_angle=1.2,
+ azimuth_angle=6,
+ twist=355,
+ )
+ .pointer_move(
+ x=10,
+ y=10,
+ origin=get_element_origin(pointerArea),
+ width=39,
+ height=35,
+ pressure=0.91,
+ altitude_angle=0.5,
+ azimuth_angle=1.8,
+ twist=345,
+ )
+ .pointer_up(button=0)
+ .pointer_move(x=80, y=50, origin=get_element_origin(pointerArea))
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_events(bidi_session, top_context["context"])
+
+ assert len(events) == 7
+ event_types = [e["type"] for e in events]
+ assert [
+ "pointerover",
+ "pointerenter",
+ "pointerdown",
+ "pointermove",
+ "pointerup",
+ "pointerout",
+ "pointerleave",
+ ] == event_types
+ assert events[2]["type"] == "pointerdown"
+ assert events[2]["tiltX"] == 20
+ assert events[2]["tiltY"] == -6
+ assert events[2]["twist"] == 355
+ assert events[3]["type"] == "pointermove"
+ assert events[3]["tiltX"] == -23
+ assert events[3]["tiltY"] == 61
+ assert events[3]["twist"] == 345
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py
new file mode 100644
index 0000000000..4f897479e2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel.py
@@ -0,0 +1,161 @@
+import pytest
+
+from webdriver.bidi.error import NoSuchFrameException
+from webdriver.bidi.modules.input import Actions, get_element_origin
+from webdriver.bidi.modules.script import ContextTarget
+
+from .. import get_events, get_object_from_context
+from . import get_shadow_root_from_test_page
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_invalid_browsing_context(bidi_session):
+ actions = Actions()
+ actions.add_wheel()
+
+ with pytest.raises(NoSuchFrameException):
+ await bidi_session.input.perform_actions(actions=actions, context="foo")
+
+
+@pytest.mark.parametrize("delta_x, delta_y", [(0, 10), (5, 0), (5, 10)])
+async def test_scroll_not_scrollable(
+ bidi_session, setup_wheel_test, top_context, get_element, delta_x, delta_y
+):
+ actions = Actions()
+
+ target = await get_element("#not-scrollable")
+ actions.add_wheel().scroll(
+ x=0, y=0, delta_x=delta_x, delta_y=delta_y, origin=get_element_origin(target)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ events = await get_events(bidi_session, top_context["context"])
+
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["deltaX"] == delta_x
+ assert events[0]["deltaY"] == delta_y
+ assert events[0]["deltaZ"] == 0
+ assert events[0]["target"] == "not-scrollable-content"
+
+
+@pytest.mark.parametrize("delta_x, delta_y", [(0, 10), (5, 0), (5, 10)])
+async def test_scroll_scrollable_overflow(
+ bidi_session, setup_wheel_test, top_context, get_element, delta_x, delta_y
+):
+ actions = Actions()
+
+ scrollable = await get_element("#scrollable")
+
+ actions.add_wheel().scroll(
+ x=0,
+ y=0,
+ delta_x=delta_x,
+ delta_y=delta_y,
+ origin=get_element_origin(scrollable),
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["deltaX"] == delta_x
+ assert events[0]["deltaY"] == delta_y
+ assert events[0]["deltaZ"] == 0
+ assert events[0]["target"] == "scrollable-content"
+
+
+@pytest.mark.parametrize("delta_x, delta_y", [(0, 10), (5, 0), (5, 10)])
+async def test_scroll_iframe(
+ bidi_session, setup_wheel_test, top_context, get_element, delta_x, delta_y
+):
+ actions = Actions()
+
+ target = await get_element("#iframe")
+ actions.add_wheel().scroll(
+ x=0, y=0, delta_x=delta_x, delta_y=delta_y, origin=get_element_origin(target)
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["deltaX"] == delta_x
+ assert events[0]["deltaY"] == delta_y
+ assert events[0]["deltaZ"] == 0
+ assert events[0]["target"] == "iframeContent"
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+async def test_scroll_shadow_tree(
+ bidi_session, top_context, get_test_page, mode, nested
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(
+ shadow_doc="""
+ <div id="scrollableShadowTree"
+ style="width: 100px; height: 100px; overflow: auto;">
+ <div
+ id="scrollableShadowTreeContent"
+ style="width: 600px; height: 1000px; background-color:blue"></div>
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ ),
+ wait="complete",
+ )
+
+ shadow_root = await get_shadow_root_from_test_page(bidi_session, top_context, nested)
+
+ # Add a simplified event recorder to track events in the test ShadowRoot.
+ scrollable = await bidi_session.script.call_function(
+ function_declaration="""shadowRoot => {
+ window.wheelEvents = [];
+ const scrollable = shadowRoot.querySelector("#scrollableShadowTree");
+ scrollable.addEventListener("wheel",
+ function(event) {
+ window.wheelEvents.push({
+ "deltaX": event.deltaX,
+ "deltaY": event.deltaY,
+ "target": event.target.id
+ });
+ }
+ );
+ return scrollable;
+ }
+ """,
+ arguments=[shadow_root],
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ actions = Actions()
+ actions.add_wheel().scroll(
+ x=0,
+ y=0,
+ delta_x=5,
+ delta_y=10,
+ origin=get_element_origin(scrollable),
+ )
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ events = await get_object_from_context(
+ bidi_session, top_context["context"], "window.wheelEvents"
+ )
+
+ assert len(events) == 1
+ assert events[0]["deltaX"] >= 5
+ assert events[0]["deltaY"] >= 10
+ assert events[0]["target"] == "scrollableShadowTreeContent"
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel_origin.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel_origin.py
new file mode 100644
index 0000000000..999b141500
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/wheel_origin.py
@@ -0,0 +1,55 @@
+import pytest
+
+from webdriver.bidi.error import NoSuchElementException
+from webdriver.bidi.modules.input import Actions, get_element_origin
+from webdriver.bidi.modules.script import ContextTarget
+
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "expression",
+ [
+ "document.querySelector('input#button').attributes[0]",
+ "document.querySelector('#with-text-node').childNodes[0]",
+ """document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")""",
+ "document.querySelector('#with-comment').childNodes[0]",
+ "document",
+ "document.doctype",
+ "document.createDocumentFragment()",
+ "document.querySelector('#custom-element').shadowRoot",
+ ],
+ ids=[
+ "attribute",
+ "text node",
+ "processing instruction",
+ "comment",
+ "document",
+ "doctype",
+ "document fragment",
+ "shadow root",
+ ]
+)
+async def test_params_actions_origin_no_such_element(
+ bidi_session, top_context, get_test_page, expression
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(),
+ wait="complete",
+ )
+
+ node = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ actions = Actions()
+ actions.add_wheel().scroll(x=0, y=0, delta_x=5, delta_y=10, origin=get_element_origin(node))
+
+ with pytest.raises(NoSuchElementException):
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/context.py b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/context.py
new file mode 100644
index 0000000000..ba2ddd1471
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/context.py
@@ -0,0 +1,42 @@
+import pytest
+from webdriver.bidi.modules.input import Actions
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.helpers import filter_supported_key_events
+from .. import get_events
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_different_top_level_contexts(
+ bidi_session, new_tab, top_context, load_static_test_page, get_focused_key_input
+):
+ await load_static_test_page(page="test_actions.html")
+ await get_focused_key_input()
+
+ actions = Actions()
+ actions.add_key().key_down("a")
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ # Reset so we only see the release events
+ await bidi_session.script.evaluate(
+ expression="resetEvents()",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ # Release actions in another context
+ await bidi_session.input.release_actions(context=new_tab["context"])
+
+ events = await get_events(bidi_session, top_context["context"])
+ assert len(events) == 0
+
+ # Release actions in right context
+ await bidi_session.input.release_actions(context=top_context["context"])
+
+ expected = [
+ {"code": "KeyA", "key": "a", "type": "keyup"},
+ ]
+ all_events = await get_events(bidi_session, top_context["context"])
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/invalid.py
new file mode 100644
index 0000000000..2adc0aa953
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/invalid.py
@@ -0,0 +1,16 @@
+import pytest
+from webdriver.bidi.error import InvalidArgumentException, NoSuchFrameException
+
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, True, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, value):
+ with pytest.raises(InvalidArgumentException):
+ await bidi_session.input.release_actions(context=value)
+
+
+async def test_params_contexts_value_invalid_value(bidi_session):
+ with pytest.raises(NoSuchFrameException):
+ await bidi_session.input.release_actions(context="foo")
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/release.py b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/release.py
new file mode 100644
index 0000000000..2955314e3c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/release.py
@@ -0,0 +1,28 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget
+
+from .. import get_events
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_release_no_actions_sends_no_events(
+ bidi_session, top_context, load_static_test_page, get_focused_key_input
+):
+ await load_static_test_page(page="test_actions.html")
+ elem = await get_focused_key_input()
+
+ await bidi_session.input.release_actions(context=top_context["context"])
+
+ keys = await bidi_session.script.call_function(
+ function_declaration="""(elem) => {
+ return elem.value;
+ }""",
+ arguments=[elem],
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ events = await get_events(bidi_session, top_context["context"])
+
+ assert len(keys["value"]) == 0
+ assert len(events) == 0
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/sequence.py b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/sequence.py
new file mode 100644
index 0000000000..603b294141
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/input/release_actions/sequence.py
@@ -0,0 +1,82 @@
+import pytest
+from webdriver.bidi.modules.input import Actions, get_element_origin
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.helpers import filter_dict, filter_supported_key_events
+from .. import get_events
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_release_char_sequence_sends_keyup_events_in_reverse(
+ bidi_session, top_context, load_static_test_page, get_focused_key_input
+):
+ await load_static_test_page(page="test_actions.html")
+ await get_focused_key_input()
+
+ actions = Actions()
+ actions.add_key().key_down("a").key_down("b")
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ # Reset so we only see the release events
+ await bidi_session.script.evaluate(
+ expression="resetEvents()",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ await bidi_session.input.release_actions(context=top_context["context"])
+ expected = [
+ {"code": "KeyB", "key": "b", "type": "keyup"},
+ {"code": "KeyA", "key": "a", "type": "keyup"},
+ ]
+ all_events = await get_events(bidi_session, top_context["context"])
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+
+@pytest.mark.parametrize(
+ "release_actions",
+ [True, False],
+ ids=["with release actions", "without release actions"],
+)
+async def test_release_mouse_sequence_resets_dblclick_state(
+ bidi_session,
+ top_context,
+ get_element,
+ load_static_test_page,
+ release_actions
+):
+ await load_static_test_page(page="test_actions.html")
+ reporter = await get_element("#outer")
+
+ actions = Actions()
+ actions.add_pointer(pointer_type="mouse").pointer_move(
+ x=0, y=0, origin=get_element_origin(reporter)
+ ).pointer_down(button=0).pointer_up(button=0)
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+
+ if release_actions:
+ await bidi_session.input.release_actions(context=top_context["context"])
+
+ await bidi_session.input.perform_actions(
+ actions=actions, context=top_context["context"]
+ )
+ events = await get_events(bidi_session, top_context["context"])
+
+ # The expeced data here might vary between the vendors since the spec at the moment
+ # is not clear on how the double/triple click should be tracked. It should be
+ # clarified in the scope of https://github.com/w3c/webdriver/issues/1772.
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ ]
+
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/log/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/__init__.py
new file mode 100644
index 0000000000..6bc6ebc407
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/__init__.py
@@ -0,0 +1,129 @@
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import (
+ any_int,
+ any_list,
+ any_string,
+ create_console_api_message,
+ recursive_compare,
+)
+
+
+def assert_base_entry(
+ entry,
+ level=any_string,
+ text=any_string,
+ timestamp=any_int,
+ realm=any_string,
+ context=None,
+ stacktrace=None
+):
+ recursive_compare({
+ "level": level,
+ "text": text,
+ "timestamp": timestamp,
+ "source": {
+ "realm": realm
+ }
+ }, entry)
+
+ if stacktrace is not None:
+ assert "stackTrace" in entry
+ assert isinstance(entry["stackTrace"], object)
+ assert "callFrames" in entry["stackTrace"]
+
+ call_frames = entry["stackTrace"]["callFrames"]
+ assert isinstance(call_frames, list)
+ assert len(call_frames) == len(stacktrace)
+ for index in range(0, len(call_frames)):
+ assert call_frames[index] == stacktrace[index]
+
+ source = entry["source"]
+ if context is not None:
+ assert "context" in source
+ assert source["context"] == context
+
+
+def assert_console_entry(
+ entry,
+ method=any_string,
+ level=any_string,
+ text=any_string,
+ args=any_list,
+ timestamp=any_int,
+ realm=any_string,
+ context=None,
+ stacktrace=None
+):
+ assert_base_entry(
+ entry=entry,
+ level=level,
+ text=text,
+ timestamp=timestamp,
+ realm=realm,
+ context=context,
+ stacktrace=stacktrace)
+
+ recursive_compare({
+ "type": "console",
+ "method": method,
+ "args": args
+ }, entry)
+
+
+def assert_javascript_entry(
+ entry,
+ level=any_string,
+ text=any_string,
+ timestamp=any_int,
+ realm=any_string,
+ context=None,
+ stacktrace=None
+):
+ assert_base_entry(
+ entry=entry,
+ level=level,
+ text=text,
+ timestamp=timestamp,
+ realm=realm,
+ stacktrace=stacktrace,
+ context=context)
+
+ recursive_compare({
+ "type": "javascript",
+ }, entry)
+
+
+async def create_console_api_message_from_string(bidi_session, context, type, value):
+ await bidi_session.script.evaluate(
+ expression=f"""console.{type}({value})""",
+ await_promise=False,
+ target=ContextTarget(context["context"]),
+ )
+
+
+async def create_javascript_error(bidi_session, context, error_message="foo"):
+ str_remote_value = {"type": "string", "value": error_message}
+
+ result = await bidi_session.script.call_function(
+ function_declaration="""(error_message) => {
+ const script = document.createElement("script");
+ script.append(document.createTextNode(`(() => { throw new Error("${error_message}") })()`));
+ document.body.append(script);
+
+ const err = new Error(error_message);
+ return err.toString();
+ }""",
+ arguments=[str_remote_value],
+ await_promise=False,
+ target=ContextTarget(context["context"]),
+ )
+
+ return result["value"]
+
+
+def create_log(bidi_session, context, log_type, text="foo"):
+ if log_type == "console_api_log":
+ return create_console_api_message(bidi_session, context, text)
+ if log_type == "javascript_error":
+ return create_javascript_error(bidi_session, context, text)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console.py
new file mode 100644
index 0000000000..061e85b7b6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console.py
@@ -0,0 +1,191 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget
+
+from . import assert_console_entry, create_console_api_message_from_string
+from ... import any_string, int_interval
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "log_argument, expected_text",
+ [
+ ("'TEST'", "TEST"),
+ ("'TWO', 'PARAMETERS'", "TWO PARAMETERS"),
+ ("{}", any_string),
+ ("['1', '2', '3']", any_string),
+ ("null, undefined", "null undefined"),
+ ],
+ ids=[
+ "single string",
+ "two strings",
+ "empty object",
+ "array of strings",
+ "null and undefined",
+ ],
+)
+async def test_text_with_argument_variation(
+ bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe, log_argument, expected_text,
+):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, "log", log_argument)
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ assert_console_entry(event_data, text=expected_text, context=top_context["context"])
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "log_method, expected_level",
+ [
+ ("assert", "error"),
+ ("debug", "debug"),
+ ("error", "error"),
+ ("info", "info"),
+ ("log", "info"),
+ ("table", "info"),
+ ("trace", "debug"),
+ ("warn", "warn"),
+ ],
+)
+async def test_level(
+ bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe, log_method, expected_level
+):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+
+ if log_method == "assert":
+ # assert has to be called with a first falsy argument to trigger a log.
+ await create_console_api_message_from_string(
+ bidi_session, top_context, "assert", "false, 'foo'")
+ else:
+ await create_console_api_message_from_string(
+ bidi_session, top_context, log_method, "'foo'")
+
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ assert_console_entry(
+ event_data, text="foo", level=expected_level, method=log_method
+ )
+
+
+@pytest.mark.asyncio
+async def test_timestamp(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe, current_time):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+
+ time_start = await current_time()
+
+ script = """new Promise(resolve => {
+ setTimeout(() => {
+ console.log('foo');
+ resolve();
+ }, 100);
+ });
+ """
+ await bidi_session.script.evaluate(
+ expression=script,
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ time_end = await current_time()
+
+ assert_console_entry(event_data, text="foo", timestamp=int_interval(time_start, time_end))
+
+
+@pytest.mark.asyncio
+async def test_method_timeEnd(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+
+ script = "console.time('test'); console.timeEnd('test');"
+
+ await bidi_session.script.evaluate(
+ expression=script,
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ assert_console_entry(event_data, method="timeEnd")
+
+
+@pytest.mark.asyncio
+async def test_new_context_with_new_window(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, 'log', "'foo'")
+ event_data = await wait_for_future_safe(on_entry_added)
+ assert_console_entry(event_data, text="foo", context=top_context["context"])
+
+ new_context = await bidi_session.browsing_context.create(type_hint="tab")
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, new_context, 'log', "'foo_in_new_window'")
+ event_data = await wait_for_future_safe(on_entry_added)
+ assert_console_entry(event_data, text="foo_in_new_window", context=new_context["context"])
+
+
+@pytest.mark.asyncio
+async def test_new_context_with_refresh(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, 'log', "'foo'")
+ event_data = await wait_for_future_safe(on_entry_added)
+ assert_console_entry(event_data, text="foo", context=top_context["context"])
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=top_context["url"], wait="complete"
+ )
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, 'log', "'foo_after_refresh'")
+ event_data = await wait_for_future_safe(on_entry_added)
+ assert_console_entry(
+ event_data, text="foo_after_refresh", context=top_context["context"]
+ )
+
+
+@pytest.mark.asyncio
+async def test_different_contexts(
+ bidi_session,
+ subscribe_events,
+ top_context,
+ wait_for_event,
+ wait_for_future_safe,
+ test_page_same_origin_frame,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_same_origin_frame, wait="complete"
+ )
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ assert len(contexts[0]["children"]) == 1
+ frame_context = contexts[0]["children"][0]
+
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, "log", "'foo'")
+ event_data = await wait_for_future_safe(on_entry_added)
+ assert_console_entry(event_data, text="foo", context=top_context["context"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, frame_context, "log", "'bar'")
+ event_data = await wait_for_future_safe(on_entry_added)
+ assert_console_entry(event_data, text="bar", context=frame_context["context"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console_args.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console_args.py
new file mode 100644
index 0000000000..ffb5183d83
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/console_args.py
@@ -0,0 +1,274 @@
+import pytest
+
+from . import assert_console_entry, create_console_api_message_from_string
+from ... import any_string
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("data,remote_value", [
+ ("undefined", {"type": "undefined"}),
+ ("null", {"type": "null"}),
+ ("'bar'", {"type": "string", "value": "bar"}),
+ ("42", {"type": "number", "value": 42}),
+ ("Number.NaN", {"type": "number", "value": "NaN"}),
+ ("-0", {"type": "number", "value": "-0"}),
+ ("Number.POSITIVE_INFINITY", {"type": "number", "value": "Infinity"}),
+ ("Number.NEGATIVE_INFINITY", {"type": "number", "value": "-Infinity"}),
+ ("false", {"type": "boolean", "value": False}),
+ ("42n", {"type": "bigint", "value": "42"}),
+], ids=[
+ "undefined",
+ "null",
+ "string",
+ "number",
+ "NaN",
+ "-0",
+ "Infinity",
+ "-Infinity",
+ "boolean",
+ "bigint",
+])
+async def test_primitive_types(
+ bidi_session, subscribe_events, top_context, wait_for_event,
+ wait_for_future_safe, data, remote_value
+):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, "log", f"'foo', {data}")
+ event_data = await wait_for_future_safe(on_entry_added)
+ args = [
+ {"type": "string", "value": "foo"},
+ {"type": remote_value["type"]},
+ ]
+ if "value" in remote_value:
+ args[1].update({"value": remote_value["value"]})
+
+ # First arg is always the first argument as provided to console.log()
+ assert_console_entry(event_data, args=args)
+
+
+@pytest.mark.parametrize(
+ "data, remote_value",
+ [
+ (
+ "(Symbol('foo'))",
+ {
+ "type": "symbol",
+ },
+ ),
+ (
+ "[1, 'foo', true, new RegExp(/foo/g), [1]]",
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "string", "value": "foo"},
+ {"type": "boolean", "value": True},
+ {
+ "type": "regexp",
+ "value": {
+ "pattern": "foo",
+ "flags": "g",
+ },
+ },
+ {"type": "array", "value": [{"type": "number", "value": 1}]},
+ ],
+ },
+ ),
+ (
+ "({'foo': {'bar': 'baz'}, 'qux': 'quux'})",
+ {
+ "type": "object",
+ "value": [
+ ["foo", {"type": "object", "value": [['bar', {"type": "string", "value": "baz"}]]}],
+ ["qux", {"type": "string", "value": "quux"}],
+ ],
+ },
+ ),
+ (
+ "(function(){})",
+ {
+ "type": "function",
+ },
+ ),
+ (
+ "new RegExp(/foo/g)",
+ {
+ "type": "regexp",
+ "value": {
+ "pattern": "foo",
+ "flags": "g",
+ },
+ },
+ ),
+ (
+ "new Date(1654004849000)",
+ {
+ "type": "date",
+ "value": "2022-05-31T13:47:29.000Z",
+ },
+ ),
+ (
+ "new Map([[1, 2], ['foo', 'bar'], [true, false], ['baz', [1]]])",
+ {
+ "type": "map",
+ "value": [
+ [
+ {"type": "number", "value": 1},
+ {"type": "number", "value": 2},
+ ],
+ ["foo", {"type": "string", "value": "bar"}],
+ [
+ {"type": "boolean", "value": True},
+ {"type": "boolean", "value": False},
+ ],
+ [
+ "baz",
+ {"type": "array", "value": [{"type": "number", "value": 1}]},
+ ],
+ ],
+ },
+ ),
+ (
+ "new Set([1, 'foo', true, [1]])",
+ {
+ "type": "set",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "string", "value": "foo"},
+ {"type": "boolean", "value": True},
+ {"type": "array", "value": [{"type": "number", "value": 1}]},
+ ],
+ },
+ ),
+ (
+ "new WeakMap()",
+ {
+ "type": "weakmap",
+ },
+ ),
+ (
+ "new WeakSet()",
+ {
+ "type": "weakset",
+ },
+ ),
+ (
+ "new Error('SOME_ERROR_TEXT')",
+ {"type": "error"},
+ ),
+ (
+ "Promise.resolve()",
+ {
+ "type": "promise",
+ },
+ ),
+ (
+ "new Int32Array()",
+ {
+ "type": "typedarray",
+ },
+ ),
+ (
+ "new ArrayBuffer()",
+ {
+ "type": "arraybuffer",
+ },
+ ),
+ (
+ "window",
+ {
+ "type": "window",
+ },
+ ),
+ (
+ "new URL('https://example.com')",
+ {
+ "type": "object",
+ },
+ ),
+ ],
+)
+async def test_remote_values(
+ bidi_session, subscribe_events, top_context, wait_for_event,
+ wait_for_future_safe, data, remote_value
+):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, "log", data
+ )
+ event_data = await wait_for_future_safe(on_entry_added)
+ arg = {"type": remote_value["type"]}
+ if "value" in remote_value:
+ arg["value"] = remote_value["value"]
+
+ # First arg is always the first argument as provided to console.log()
+ assert_console_entry(event_data, args=[arg])
+
+
+@pytest.mark.parametrize(
+ "data, expected",
+ [
+ (
+ "document.querySelector('br')",
+ [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 0,
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ },
+ ],
+ ),
+ (
+ "document.querySelector('#custom-element')",
+ [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {
+ "id": "custom-element",
+ },
+ "childNodeCount": 0,
+ "localName": "custom-element",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": {
+ "sharedId": any_string,
+ "type": "node",
+ },
+ },
+ },
+ ],
+ ),
+ ],
+ ids=["basic", "shadowRoot"],
+)
+async def test_node(
+ bidi_session, subscribe_events, get_test_page, top_context, wait_for_event,
+ wait_for_future_safe, data, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=get_test_page(), wait="complete"
+ )
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message_from_string(
+ bidi_session, top_context, "log", data
+ )
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ assert_console_entry(event_data, args=expected)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/event_buffer.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/event_buffer.py
new file mode 100644
index 0000000000..35b06a6b33
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/event_buffer.py
@@ -0,0 +1,95 @@
+import pytest
+
+from . import assert_base_entry, create_log
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("log_type", ["console_api_log", "javascript_error"])
+async def test_console_log_cached_messages(
+ bidi_session, wait_for_event, wait_for_future_safe, log_type, new_tab
+):
+ # Clear events buffer.
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+ await bidi_session.session.unsubscribe(events=["log.entryAdded"])
+
+ # Log a message before subscribing
+ expected_text = await create_log(bidi_session, new_tab, log_type, "cached_message")
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Subscribe
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+ # Cached events are emitted before the subscribe command is finished.
+ assert len(events) == 1
+
+ # Check the log.entryAdded event received has the expected properties.
+ assert_base_entry(events[0], text=expected_text, context=new_tab["context"])
+
+ # Unsubscribe and re-subscribe
+ await bidi_session.session.unsubscribe(events=["log.entryAdded"])
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+
+ # Check that the cached event was not re-emitted.
+ assert len(events) == 1
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text = await create_log(bidi_session, new_tab, log_type, "live_message")
+ await wait_for_future_safe(on_entry_added)
+
+ # Check that we only received the live message.
+ assert len(events) == 2
+ assert_base_entry(events[1], text=expected_text, context=new_tab["context"])
+
+ # Unsubscribe, log a message and re-subscribe
+ await bidi_session.session.unsubscribe(events=["log.entryAdded"])
+ expected_text = await create_log(bidi_session, new_tab, log_type, "cached_message_2")
+
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+
+ # Check that only the newly cached event was emitted
+ assert len(events) == 3
+ assert_base_entry(events[2], text=expected_text, context=new_tab["context"])
+
+ await bidi_session.session.unsubscribe(events=["log.entryAdded"])
+ remove_listener()
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("log_type", ["console_api_log", "javascript_error"])
+async def test_console_log_cached_message_after_refresh(
+ bidi_session, subscribe_events, new_tab, log_type
+):
+ # Clear events buffer.
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+ await bidi_session.session.unsubscribe(events=["log.entryAdded"])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Log a message, refresh, log another message and subscribe
+ expected_text_1 = await create_log(bidi_session, new_tab, log_type, "cached_message_1")
+ context = new_tab["context"]
+ await bidi_session.browsing_context.navigate(context=context,
+ url='about:blank',
+ wait="complete")
+ expected_text_2 = await create_log(bidi_session, new_tab, log_type, "cached_message_2")
+
+ await subscribe_events(events=["log.entryAdded"])
+
+ # Check that only the cached message was retrieved.
+ assert len(events) == 2
+ assert_base_entry(events[0], text=expected_text_1)
+ assert_base_entry(events[1], text=expected_text_2)
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/javascript.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/javascript.py
new file mode 100644
index 0000000000..f16343cfb7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/javascript.py
@@ -0,0 +1,28 @@
+import pytest
+
+from . import assert_javascript_entry, create_log
+from ... import int_interval
+
+
+@pytest.mark.asyncio
+async def test_types_and_values(
+ bidi_session, subscribe_events, current_time, top_context, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+
+ time_start = await current_time()
+
+ expected_text = await create_log(bidi_session, top_context, "javascript_error", "cached_message")
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ time_end = await current_time()
+
+ assert_javascript_entry(
+ event_data,
+ level="error",
+ text=expected_text,
+ timestamp=int_interval(time_start, time_end),
+ context=top_context["context"],
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/realm.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/realm.py
new file mode 100644
index 0000000000..4df72ff686
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/realm.py
@@ -0,0 +1,32 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget
+
+from . import assert_console_entry
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "sandbox_name",
+ ["", "sandbox_1"],
+ ids=["default realm", "sandbox"],
+)
+async def test_realm(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe, sandbox_name):
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text = "foo"
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression=f"console.log('{expected_text}')",
+ await_promise=False,
+ target=ContextTarget(top_context["context"], sandbox=sandbox_name),
+ )
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ assert_console_entry(
+ event_data,
+ text=expected_text,
+ context=top_context["context"],
+ realm=result["realm"],
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/stacktrace.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/stacktrace.py
new file mode 100644
index 0000000000..f9aab697bd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/stacktrace.py
@@ -0,0 +1,121 @@
+import pytest
+
+from . import assert_console_entry, assert_javascript_entry
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "log_method, expect_stack",
+ [
+ ("assert", True),
+ ("debug", False),
+ ("error", True),
+ ("info", False),
+ ("log", False),
+ ("table", False),
+ ("trace", True),
+ ("warn", True),
+ ],
+)
+async def test_console_entry_sync_callstack(
+ bidi_session, subscribe_events, inline, top_context, wait_for_event, wait_for_future_safe, log_method, expect_stack
+):
+ if log_method == "assert":
+ # assert has to be called with a first falsy argument to trigger a log.
+ url = inline(
+ f"""
+ <script>
+ function foo() {{ console.{log_method}(false, "cheese"); }}
+ function bar() {{ foo(); }}
+ bar();
+ </script>
+ """
+ )
+ else:
+ url = inline(
+ f"""
+ <script>
+ function foo() {{ console.{log_method}("cheese"); }}
+ function bar() {{ foo(); }}
+ bar();
+ </script>
+ """
+ )
+
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+
+ if expect_stack:
+ expected_stack = [
+ {"columnNumber": 41, "functionName": "foo", "lineNumber": 4, "url": url},
+ {"columnNumber": 33, "functionName": "bar", "lineNumber": 5, "url": url},
+ {"columnNumber": 16, "functionName": "", "lineNumber": 6, "url": url},
+ ]
+ else:
+ expected_stack = None
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ assert_console_entry(
+ event_data,
+ method=log_method,
+ text="cheese",
+ stacktrace=expected_stack,
+ context=top_context["context"],
+ )
+
+ # Navigate to a page with no error to avoid polluting the next tests with
+ # JavaScript errors.
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<p>foo"), wait="complete"
+ )
+
+
+@pytest.mark.asyncio
+async def test_javascript_entry_sync_callstack(
+ bidi_session, subscribe_events, inline, top_context, wait_for_event, wait_for_future_safe
+):
+ url = inline(
+ """
+ <script>
+ function foo() { throw new Error("cheese"); }
+ function bar() { foo(); }
+ bar();
+ </script>
+ """
+ )
+
+ await subscribe_events(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+
+ expected_stack = [
+ {"columnNumber": 35, "functionName": "foo", "lineNumber": 4, "url": url},
+ {"columnNumber": 29, "functionName": "bar", "lineNumber": 5, "url": url},
+ {"columnNumber": 12, "functionName": "", "lineNumber": 6, "url": url},
+ ]
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ assert_javascript_entry(
+ event_data,
+ level="error",
+ text="Error: cheese",
+ stacktrace=expected_stack,
+ context=top_context["context"],
+ )
+
+ # Navigate to a page with no error to avoid polluting the next tests with
+ # JavaScript errors.
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<p>foo"), wait="complete"
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/subscription.py b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/subscription.py
new file mode 100644
index 0000000000..1cb1bce38b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/log/entry_added/subscription.py
@@ -0,0 +1,110 @@
+import asyncio
+
+import pytest
+
+from . import assert_base_entry, create_log
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("log_type", ["console_api_log", "javascript_error"])
+async def test_subscribe_twice(bidi_session, new_tab, wait_for_event, wait_for_future_safe, log_type):
+ # Subscribe to log.entryAdded twice and check that events are received once.
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Check for a ConsoleLogEntry.
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text = await create_log(bidi_session, new_tab, log_type, "text1")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+ assert_base_entry(events[0], text=expected_text)
+
+ # Wait for some time and check the events array again
+ await asyncio.sleep(0.5)
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("log_type", ["console_api_log", "javascript_error"])
+async def test_subscribe_unsubscribe(bidi_session, new_tab, wait_for_event, wait_for_future_safe, log_type):
+ # Subscribe for log events globally
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_log(bidi_session, new_tab, log_type, "some text")
+ await wait_for_future_safe(on_entry_added)
+
+ # Unsubscribe from log events globally
+ await bidi_session.session.unsubscribe(events=["log.entryAdded"])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ expected_text_0 = await create_log(bidi_session, new_tab, log_type, "text_0")
+
+ # Wait for some time before checking the events array
+ await asyncio.sleep(0.5)
+ assert len(events) == 0
+
+ # Refresh to create a new context
+ context = new_tab["context"]
+ await bidi_session.browsing_context.navigate(context=context,
+ url='about:blank',
+ wait="complete")
+
+ # Check we still don't receive ConsoleLogEntry events from the new context
+ expected_text_1 = await create_log(bidi_session, new_tab, log_type, "text_1")
+
+ # Wait for some time before checking the events array
+ await asyncio.sleep(0.5)
+ assert len(events) == 0
+
+ # Refresh to create a new context. Note that we refresh to avoid getting
+ # cached events from the log event buffer.
+ context = new_tab["context"]
+ await bidi_session.browsing_context.navigate(context=context,
+ url='about:blank',
+ wait="complete")
+
+ # Check that if we subscribe again, we can receive events
+ await bidi_session.session.subscribe(events=["log.entryAdded"])
+
+ # Check buffered events are emitted.
+ assert len(events) == 2
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text_2 = await create_log(bidi_session, new_tab, log_type, "text_2")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 3
+ assert_base_entry(events[0], text=expected_text_0, context=new_tab["context"])
+ assert_base_entry(events[1], text=expected_text_1, context=new_tab["context"])
+ assert_base_entry(events[2], text=expected_text_2, context=new_tab["context"])
+
+ # Check that we also get events from a new context
+ new_context = await bidi_session.browsing_context.create(type_hint="tab")
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text_3 = await create_log(bidi_session, new_context, log_type, "text_3")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 4
+ assert_base_entry(events[3], text=expected_text_3, context=new_context["context"])
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/__init__.py
new file mode 100644
index 0000000000..9bbc6f5daf
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/__init__.py
@@ -0,0 +1,351 @@
+from .. import (
+ any_bool,
+ any_dict,
+ any_int,
+ any_int_or_null,
+ any_list,
+ any_string,
+ any_string_or_null,
+ recursive_compare,
+)
+
+
+def assert_bytes_value(bytes_value):
+ assert bytes_value["type"] in ["string", "base64"]
+ any_string(bytes_value["value"])
+
+
+def assert_cookies(event_cookies, expected_cookies):
+ assert len(event_cookies) == len(expected_cookies)
+
+ # Simple helper to find a cookie by key and value only.
+ def match_cookie(cookie, expected):
+ for key in expected:
+ if cookie[key] != expected[key]:
+ return False
+
+ return True
+
+ for cookie in expected_cookies:
+ assert next(c for c in event_cookies if match_cookie(c, cookie)) is not None
+
+
+def assert_headers(event_headers, expected_headers):
+ # The browser sets request headers, only assert that the expected headers
+ # are included in the request's headers.
+ assert len(event_headers) >= len(expected_headers)
+ for header in expected_headers:
+ assert next(h for h in event_headers if header == h) is not None
+
+
+def assert_timing_info(timing_info):
+ recursive_compare(
+ {
+ "timeOrigin": any_int,
+ "requestTime": any_int,
+ "redirectStart": any_int,
+ "redirectEnd": any_int,
+ "fetchStart": any_int,
+ "dnsStart": any_int,
+ "dnsEnd": any_int,
+ "connectStart": any_int,
+ "connectEnd": any_int,
+ "tlsStart": any_int,
+ "requestStart": any_int,
+ "responseStart": any_int,
+ "responseEnd": any_int,
+ },
+ timing_info,
+ )
+
+
+def assert_request_data(request_data, expected_request):
+ recursive_compare(
+ {
+ "bodySize": any_int_or_null,
+ "cookies": any_list,
+ "headers": any_list,
+ "headersSize": any_int,
+ "method": any_string,
+ "request": any_string,
+ "timings": any_dict,
+ "url": any_string,
+ },
+ request_data,
+ )
+
+ assert_timing_info(request_data["timings"])
+
+ for cookie in request_data["cookies"]:
+ assert_bytes_value(cookie["value"])
+
+ if "cookies" in expected_request:
+ assert_cookies(request_data["cookies"], expected_request["cookies"])
+ # While recursive_compare tolerates missing entries in dict, arrays
+ # need to have the exact same number of items, and be in the same order.
+ # We don't want to assert all headers and cookies, so we do a custom
+ # assert for each and then delete it before using recursive_compare.
+ del expected_request["cookies"]
+
+ for header in request_data["headers"]:
+ assert_bytes_value(header["value"])
+
+ if "headers" in expected_request:
+ assert_headers(request_data["headers"], expected_request["headers"])
+ # Remove headers before using recursive_compare, see comment for cookies
+ del expected_request["headers"]
+
+ recursive_compare(expected_request, request_data)
+
+
+def assert_base_parameters(
+ event,
+ context=None,
+ intercepts=None,
+ is_blocked=None,
+ navigation=None,
+ redirect_count=None,
+ expected_request=None,
+):
+ recursive_compare(
+ {
+ "context": any_string_or_null,
+ "isBlocked": any_bool,
+ "navigation": any_string_or_null,
+ "redirectCount": any_int,
+ "request": any_dict,
+ "timestamp": any_int,
+ },
+ event,
+ )
+
+ if context is not None:
+ assert event["context"] == context
+
+ if is_blocked is not None:
+ assert event["isBlocked"] == is_blocked
+
+ if event["isBlocked"]:
+ assert isinstance(event["intercepts"], list)
+ assert len(event["intercepts"]) > 0
+ for intercept in event["intercepts"]:
+ assert isinstance(intercept, str)
+ else:
+ assert "intercepts" not in event
+
+ if intercepts is not None:
+ assert event["intercepts"] == intercepts
+
+ if navigation is not None:
+ assert event["navigation"] == navigation
+
+ if redirect_count is not None:
+ assert event["redirectCount"] == redirect_count
+
+ # Assert request data
+ if expected_request is not None:
+ assert_request_data(event["request"], expected_request)
+
+
+def assert_before_request_sent_event(
+ event,
+ context=None,
+ intercepts=None,
+ is_blocked=None,
+ navigation=None,
+ redirect_count=None,
+ expected_request=None,
+):
+ # Assert initiator
+ assert isinstance(event["initiator"], dict)
+ assert isinstance(event["initiator"]["type"], str)
+
+ # Assert base parameters
+ assert_base_parameters(
+ event,
+ context=context,
+ intercepts=intercepts,
+ is_blocked=is_blocked,
+ navigation=navigation,
+ redirect_count=redirect_count,
+ expected_request=expected_request,
+ )
+
+
+def assert_fetch_error_event(
+ event,
+ context=None,
+ errorText=None,
+ intercepts=None,
+ is_blocked=None,
+ navigation=None,
+ redirect_count=None,
+ expected_request=None,
+):
+ # Assert errorText
+ assert isinstance(event["errorText"], str)
+
+ if errorText is not None:
+ assert event["errorText"] == errorText
+
+ # Assert base parameters
+ assert_base_parameters(
+ event,
+ context=context,
+ intercepts=intercepts,
+ is_blocked=is_blocked,
+ navigation=navigation,
+ redirect_count=redirect_count,
+ expected_request=expected_request,
+ )
+
+
+def assert_response_data(response_data, expected_response):
+ recursive_compare(
+ {
+ "bodySize": any_int_or_null,
+ "bytesReceived": any_int,
+ "content": {
+ "size": any_int_or_null,
+ },
+ "fromCache": any_bool,
+ "headersSize": any_int_or_null,
+ "protocol": any_string,
+ "status": any_int,
+ "statusText": any_string,
+ "url": any_string,
+ },
+ response_data,
+ )
+
+ for header in response_data["headers"]:
+ assert_bytes_value(header["value"])
+
+ for header in response_data["headers"]:
+ assert_bytes_value(header["value"])
+
+ if "headers" in expected_response:
+ assert_headers(response_data["headers"], expected_response["headers"])
+ # Remove headers before using recursive_compare, see comment for cookies
+ # in assert_request_data
+ del expected_response["headers"]
+
+ if response_data["status"] in [401, 407]:
+ assert isinstance(response_data["authChallenges"], list)
+ else:
+ assert "authChallenges" not in response_data
+
+ recursive_compare(expected_response, response_data)
+
+
+def assert_response_event(
+ event,
+ context=None,
+ intercepts=None,
+ is_blocked=None,
+ navigation=None,
+ redirect_count=None,
+ expected_request=None,
+ expected_response=None,
+):
+ # Assert response data
+ any_dict(event["response"])
+ if expected_response is not None:
+ assert_response_data(event["response"], expected_response)
+
+ # Assert base parameters
+ assert_base_parameters(
+ event,
+ context=context,
+ intercepts=intercepts,
+ is_blocked=is_blocked,
+ navigation=navigation,
+ redirect_count=redirect_count,
+ expected_request=expected_request,
+ )
+
+
+# Create a simple cookie or set-cookie header. They share the same structure
+# as a regular header, so this is simple alias for create_header.
+def create_cookie_header(overrides=None, value_overrides=None):
+ return create_header(overrides, value_overrides)
+
+
+# Create a simple header dict, with mandatory name and value keys.
+# Use the `overrides` argument to update the values of those properties, or to
+# add new top-level keys.
+# Use the `value_overrides` argument to update keys nested in the `value` dict.
+def create_header(overrides=None, value_overrides=None):
+ header = {
+ "name": "test",
+ "value": {
+ "type": "string",
+ "value": "foo"
+ }
+ }
+
+ if overrides is not None:
+ header.update(overrides)
+
+ if value_overrides is not None:
+ header["value"].update(value_overrides)
+
+ return header
+
+
+# Array of status and status text expected to be available in network events
+HTTP_STATUS_AND_STATUS_TEXT = [
+ (101, "Switching Protocols"),
+ (200, "OK"),
+ (201, "Created"),
+ (202, "Accepted"),
+ (203, "Non-Authoritative Information"),
+ (204, "No Content"),
+ (205, "Reset Content"),
+ (206, "Partial Content"),
+ (300, "Multiple Choices"),
+ (301, "Moved Permanently"),
+ (302, "Found"),
+ (303, "See Other"),
+ (305, "Use Proxy"),
+ (307, "Temporary Redirect"),
+ (400, "Bad Request"),
+ (401, "Unauthorized"),
+ (402, "Payment Required"),
+ (403, "Forbidden"),
+ (404, "Not Found"),
+ (405, "Method Not Allowed"),
+ (406, "Not Acceptable"),
+ (407, "Proxy Authentication Required"),
+ (408, "Request Timeout"),
+ (409, "Conflict"),
+ (410, "Gone"),
+ (411, "Length Required"),
+ (412, "Precondition Failed"),
+ (415, "Unsupported Media Type"),
+ (417, "Expectation Failed"),
+ (500, "Internal Server Error"),
+ (501, "Not Implemented"),
+ (502, "Bad Gateway"),
+ (503, "Service Unavailable"),
+ (504, "Gateway Timeout"),
+ (505, "HTTP Version Not Supported"),
+]
+
+PAGE_EMPTY_HTML = "/webdriver/tests/bidi/network/support/empty.html"
+PAGE_EMPTY_IMAGE = "/webdriver/tests/bidi/network/support/empty.png"
+PAGE_EMPTY_SCRIPT = "/webdriver/tests/bidi/network/support/empty.js"
+PAGE_EMPTY_SVG = "/webdriver/tests/bidi/network/support/empty.svg"
+PAGE_EMPTY_TEXT = "/webdriver/tests/bidi/network/support/empty.txt"
+PAGE_INVALID_URL = "https://not_a_valid_url.test/"
+PAGE_OTHER_TEXT = "/webdriver/tests/bidi/network/support/other.txt"
+PAGE_REDIRECT_HTTP_EQUIV = (
+ "/webdriver/tests/bidi/network/support/redirect_http_equiv.html"
+)
+PAGE_REDIRECTED_HTML = "/webdriver/tests/bidi/network/support/redirected.html"
+
+AUTH_REQUIRED_EVENT = "network.authRequired"
+BEFORE_REQUEST_SENT_EVENT = "network.beforeRequestSent"
+FETCH_ERROR_EVENT = "network.fetchError"
+RESPONSE_COMPLETED_EVENT = "network.responseCompleted"
+RESPONSE_STARTED_EVENT = "network.responseStarted"
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/add_intercept.py b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/add_intercept.py
new file mode 100644
index 0000000000..7648eb1934
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/add_intercept.py
@@ -0,0 +1,170 @@
+import asyncio
+import uuid
+
+import pytest
+from webdriver.bidi.modules.script import ScriptEvaluateResultException
+
+from .. import (
+ assert_before_request_sent_event,
+ PAGE_EMPTY_HTML,
+ PAGE_EMPTY_TEXT,
+ PAGE_OTHER_TEXT,
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ RESPONSE_STARTED_EVENT,
+)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("phase", ["beforeRequestSent", "responseStarted"])
+async def test_other_context(
+ bidi_session,
+ url,
+ top_context,
+ add_intercept,
+ fetch,
+ setup_network_test,
+ phase,
+):
+ # Subscribe to network events only in top_context
+ await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ],
+ contexts=[top_context["context"]],
+ )
+
+ # Create another tab, where network events are not monitored.
+ other_context = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.browsing_context.navigate(
+ context=other_context["context"], url=url(PAGE_EMPTY_HTML), wait="complete"
+ )
+
+ # Add an intercept.
+ text_url = url(PAGE_EMPTY_TEXT)
+ await add_intercept(
+ phases=[phase],
+ url_patterns=[{"type": "string", "pattern": text_url}],
+ )
+
+ # Request to top_context should be blocked and throw a ScriptEvaluateResultException
+ # from the AbortController.
+ with pytest.raises(ScriptEvaluateResultException):
+ await fetch(text_url, context=top_context)
+
+ # Request to other_context should not be blocked.
+ await fetch(text_url, context=other_context)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("phase", ["beforeRequestSent", "responseStarted"])
+async def test_other_url(
+ url,
+ add_intercept,
+ fetch,
+ setup_network_test,
+ phase,
+):
+ await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ],
+ )
+
+ # Add an intercept.
+ text_url = url(PAGE_EMPTY_TEXT)
+ await add_intercept(
+ phases=[phase],
+ url_patterns=[{"type": "string", "pattern": text_url}],
+ )
+
+ # Request to PAGE_EMPTY_TEXT should be blocked and throw a ScriptEvaluateResultException
+ # from the AbortController.
+ with pytest.raises(ScriptEvaluateResultException):
+ await fetch(text_url)
+
+ # Request to PAGE_OTHER_TEXT should not be blocked.
+ await fetch(url(PAGE_OTHER_TEXT))
+
+
+@pytest.mark.asyncio
+async def test_return_value(add_intercept):
+ intercept = await add_intercept(phases=["beforeRequestSent"], url_patterns=[])
+
+ assert isinstance(intercept, str)
+ uuid.UUID(hex=intercept)
+
+
+@pytest.mark.asyncio
+async def test_two_intercepts(
+ bidi_session,
+ wait_for_event,
+ url,
+ add_intercept,
+ fetch,
+ setup_network_test,
+ wait_for_future_safe,
+):
+ await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ],
+ )
+
+ # Add a string intercept to catch requests to PAGE_EMPTY_TEXT.
+ text_url = url(PAGE_EMPTY_TEXT)
+ string_intercept = await add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "string", "pattern": text_url}],
+ )
+ # Add a second intercept to catch all requests.
+ global_intercept = await add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[],
+ )
+
+ # Perform a request to PAGE_EMPTY_TEXT, which should match both intercepts
+ on_network_event = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ asyncio.ensure_future(fetch(text_url))
+ event = await wait_for_future_safe(on_network_event)
+
+ assert_before_request_sent_event(
+ event, is_blocked=True, intercepts=[string_intercept, global_intercept]
+ )
+
+ # Perform a request to PAGE_OTHER_TEXT, which should only match one intercept
+ other_url = url(PAGE_OTHER_TEXT)
+
+ on_network_event = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ asyncio.ensure_future(fetch(other_url))
+ event = await wait_for_future_safe(on_network_event)
+
+ assert_before_request_sent_event(
+ event, is_blocked=True, intercepts=[global_intercept]
+ )
+
+ # Remove the global intercept, requests to PAGE_OTHER_TEXT should no longer
+ # be blocked.
+ await bidi_session.network.remove_intercept(intercept=global_intercept)
+ await fetch(other_url)
+
+ # Requests to PAGE_EMPTY_TEXT should still be blocked, but only by one
+ # intercept.
+ on_network_event = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ asyncio.ensure_future(fetch(text_url))
+ event = await wait_for_future_safe(on_network_event)
+
+ assert_before_request_sent_event(
+ event, is_blocked=True, intercepts=[string_intercept]
+ )
+
+ # Remove the string intercept, requests to PAGE_EMPTY_TEXT should no longer
+ # be blocked.
+ await bidi_session.network.remove_intercept(intercept=string_intercept)
+ await fetch(text_url)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/invalid.py
new file mode 100644
index 0000000000..ac7b273854
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/invalid.py
@@ -0,0 +1,187 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, "foo", False, 42, {}])
+async def test_params_phases_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(phases=value)
+
+
+async def test_params_phases_invalid_value_empty_array(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(phases=[])
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_phases_entry_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(phases=[value])
+
+
+@pytest.mark.parametrize("value", ["foo", "responseCompleted"])
+async def test_params_phases_entry_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(phases=[value])
+
+
+@pytest.mark.parametrize("value", ["foo", False, 42, {}])
+async def test_params_url_patterns_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"], url_patterns=value
+ )
+
+
+@pytest.mark.parametrize("value", [None, "foo", False, 42, []])
+async def test_params_url_patterns_entry_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"], url_patterns=[value]
+ )
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "foo"}])
+async def test_params_url_patterns_entry_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"], url_patterns=[value]
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+async def test_params_url_patterns_string_pattern_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "string", "pattern": value}],
+ )
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ "foo",
+ "*",
+ "(",
+ ")",
+ "{",
+ "}",
+ "http\\{s\\}://example.com",
+ "https://example.com:port/",
+ ],
+)
+async def test_params_url_patterns_string_pattern_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "string", "pattern": value}],
+ )
+
+
+@pytest.mark.parametrize(
+ "property", ["protocol", "hostname", "port", "pathname", "search"]
+)
+@pytest.mark.parametrize("value", [False, 42, [], {}])
+async def test_params_url_patterns_pattern_property_invalid_type(
+ bidi_session, property, value
+):
+ with pytest.raises(error.InvalidArgumentException):
+ url_pattern = {"type": "pattern"}
+ url_pattern[property] = value
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[url_pattern],
+ )
+
+
+@pytest.mark.parametrize(
+ "property", ["protocol", "hostname", "port", "pathname", "search"]
+)
+@pytest.mark.parametrize("value", ["*", "(", ")", "{", "}"])
+async def test_params_url_patterns_pattern_property_unescaped_character(
+ bidi_session, property, value
+):
+ with pytest.raises(error.InvalidArgumentException):
+ url_pattern = {"type": "pattern"}
+ url_pattern[property] = value
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[url_pattern],
+ )
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ "",
+ "http/",
+ "http\\*",
+ "http\\(",
+ "http\\)",
+ "http\\{",
+ "http\\}",
+ "http#",
+ "http@",
+ "http%",
+ ],
+)
+async def test_params_url_patterns_pattern_protocol_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "pattern", "protocol": value}],
+ )
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ "file",
+ "file:",
+ ],
+)
+async def test_params_url_patterns_pattern_protocol_file_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "pattern", "protocol": value, "hostname": "example.com"}],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "abc/com/", "abc?com", "abc#com", "abc:com", "abc::com", "::1"])
+async def test_params_url_patterns_pattern_hostname_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "pattern", "hostname": value}],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "abcd", "-1", "80 ", "1.3", ":80", "80:", "65536"])
+async def test_params_url_patterns_pattern_port_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "pattern", "port": value}],
+ )
+
+
+@pytest.mark.parametrize("value", ["path?", "path#"])
+async def test_params_url_patterns_pattern_pathname_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "pattern", "pathname": value}],
+ )
+
+
+@pytest.mark.parametrize("value", ["search#"])
+async def test_params_url_patterns_pattern_search_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "pattern", "search": value}],
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phase_auth_required.py b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phase_auth_required.py
new file mode 100644
index 0000000000..dd322a2340
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phase_auth_required.py
@@ -0,0 +1,145 @@
+import pytest
+
+from .. import (
+ assert_before_request_sent_event,
+ assert_response_event,
+)
+
+from .. import (
+ assert_before_request_sent_event,
+ assert_response_event,
+ PAGE_EMPTY_TEXT,
+ AUTH_REQUIRED_EVENT,
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ RESPONSE_STARTED_EVENT,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_basic_authentication(
+ bidi_session,
+ new_tab,
+ wait_for_event,
+ wait_for_future_safe,
+ url,
+ setup_network_test,
+ add_intercept,
+):
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ AUTH_REQUIRED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ before_request_sent_events = network_events[BEFORE_REQUEST_SENT_EVENT]
+ response_started_events = network_events[RESPONSE_STARTED_EVENT]
+ auth_required_events = network_events[AUTH_REQUIRED_EVENT]
+ response_completed_events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ auth_url = url("/webdriver/tests/support/http_handlers/authentication.py")
+ intercept = await add_intercept(
+ phases=["authRequired"],
+ url_patterns=[{"type": "string", "pattern": auth_url}],
+ )
+
+ assert isinstance(intercept, str)
+
+ on_auth_required = wait_for_event(AUTH_REQUIRED_EVENT)
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=auth_url,
+ wait="none",
+ )
+
+ await wait_for_future_safe(on_auth_required)
+ expected_request = {"method": "GET", "url": auth_url}
+
+ assert len(before_request_sent_events) == 1
+ assert len(response_started_events) == 1
+ assert len(auth_required_events) == 1
+
+ assert_before_request_sent_event(
+ before_request_sent_events[0],
+ expected_request=expected_request,
+ is_blocked=False,
+ )
+ assert_response_event(
+ response_started_events[0],
+ expected_request=expected_request,
+ is_blocked=False,
+ )
+ assert_response_event(
+ auth_required_events[0],
+ expected_request=expected_request,
+ is_blocked=True,
+ intercepts=[intercept],
+ )
+
+ # The request should remain blocked at the authRequired phase.
+ assert len(response_completed_events) == 0
+
+
+async def test_no_authentication(
+ wait_for_event,
+ url,
+ setup_network_test,
+ add_intercept,
+ fetch,
+ wait_for_future_safe,
+):
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ AUTH_REQUIRED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ before_request_sent_events = network_events[BEFORE_REQUEST_SENT_EVENT]
+ response_started_events = network_events[RESPONSE_STARTED_EVENT]
+ auth_required_events = network_events[AUTH_REQUIRED_EVENT]
+ response_completed_events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ intercept = await add_intercept(
+ phases=["authRequired"],
+ url_patterns=[{"type": "string", "pattern": text_url}],
+ )
+
+ assert isinstance(intercept, str)
+
+ on_network_event = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ await fetch(text_url)
+ await wait_for_future_safe(on_network_event)
+
+ expected_request = {"method": "GET", "url": text_url}
+
+ assert len(before_request_sent_events) == 1
+ assert len(response_started_events) == 1
+ assert len(response_completed_events) == 1
+
+ # Check that no network event was blocked because of the authRequired
+ # intercept since the URL does not trigger an auth prompt.
+ assert_before_request_sent_event(
+ before_request_sent_events[0],
+ expected_request=expected_request,
+ is_blocked=False,
+ )
+ assert_response_event(
+ response_started_events[0],
+ expected_request=expected_request,
+ is_blocked=False,
+ )
+ assert_response_event(
+ response_completed_events[0],
+ expected_request=expected_request,
+ is_blocked=False,
+ )
+
+ # No authRequired event should have been received.
+ assert len(auth_required_events) == 0
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phases.py b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phases.py
new file mode 100644
index 0000000000..868f03041e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/phases.py
@@ -0,0 +1,121 @@
+import pytest
+from webdriver.bidi.modules.script import ScriptEvaluateResultException
+
+from .. import (
+ assert_before_request_sent_event,
+ assert_response_event,
+ PAGE_EMPTY_TEXT,
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ RESPONSE_STARTED_EVENT,
+)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "phases, intercepted_phase",
+ [
+ (["beforeRequestSent"], "beforeRequestSent"),
+ (["responseStarted"], "responseStarted"),
+ (["beforeRequestSent", "responseStarted"], "beforeRequestSent"),
+ (["responseStarted", "beforeRequestSent"], "beforeRequestSent"),
+ (["beforeRequestSent", "beforeRequestSent"], "beforeRequestSent"),
+ ],
+)
+async def test_request_response_phases(
+ wait_for_event,
+ url,
+ setup_network_test,
+ add_intercept,
+ fetch,
+ wait_for_future_safe,
+ phases,
+ intercepted_phase,
+):
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ before_request_sent_events = network_events[BEFORE_REQUEST_SENT_EVENT]
+ response_started_events = network_events[RESPONSE_STARTED_EVENT]
+ response_completed_events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ intercept = await add_intercept(
+ phases=phases,
+ url_patterns=[{"type": "string", "pattern": text_url}],
+ )
+
+ assert isinstance(intercept, str)
+
+ on_network_event = wait_for_event(f"network.{intercepted_phase}")
+
+ # Request to top_context should be blocked and throw a ScriptEvaluateResultException
+ # from the AbortController.
+ with pytest.raises(ScriptEvaluateResultException):
+ await fetch(text_url)
+
+ await wait_for_future_safe(on_network_event)
+ expected_request = {"method": "GET", "url": text_url}
+
+ if intercepted_phase == "beforeRequestSent":
+ assert len(before_request_sent_events) == 1
+ assert len(response_started_events) == 0
+ assert_before_request_sent_event(
+ before_request_sent_events[0],
+ expected_request=expected_request,
+ is_blocked=True,
+ intercepts=[intercept],
+ )
+ elif intercepted_phase == "responseStarted":
+ assert len(before_request_sent_events) == 1
+ assert len(response_started_events) == 1
+ assert_before_request_sent_event(
+ before_request_sent_events[0],
+ expected_request=expected_request,
+ is_blocked=False,
+ )
+ assert_response_event(
+ response_started_events[0],
+ expected_request=expected_request,
+ is_blocked=True,
+ intercepts=[intercept],
+ )
+
+ # Check that we did not receive response completed events.
+ assert len(response_completed_events) == 0
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("phase", ["beforeRequestSent", "responseStarted"])
+async def test_not_listening_to_phase_event(
+ url,
+ setup_network_test,
+ add_intercept,
+ fetch,
+ phase,
+):
+ events = [
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+
+ # Remove the event corresponding to the intercept phase from the monitored
+ # events.
+ events.remove(f"network.{phase}")
+
+ await setup_network_test(events=events)
+
+ # Add an intercept without listening to the corresponding network event
+ text_url = url(PAGE_EMPTY_TEXT)
+ await add_intercept(
+ phases=[phase],
+ url_patterns=[{"type": "string", "pattern": text_url}],
+ )
+
+ # Request should not be blocked.
+ await fetch(text_url)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/url_patterns.py b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/url_patterns.py
new file mode 100644
index 0000000000..517a94ffc4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/add_intercept/url_patterns.py
@@ -0,0 +1,220 @@
+import asyncio
+
+import pytest
+
+from .. import assert_before_request_sent_event, BEFORE_REQUEST_SENT_EVENT
+
+
+@pytest.fixture
+def substitute_host(server_config):
+ """This test will perform various requests which should not reach the
+ external network. All strings refering to a domain will define it as a
+ placeholder which needs to be dynamically replaced by a value from the
+ current server configuration"""
+
+ def substitute_host(str):
+ wpt_host = server_config["browser_host"]
+ return str.format(
+ wpt_host=wpt_host,
+ wpt_host_upper=wpt_host.upper(),
+ )
+
+ return substitute_host
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "patterns, url_template",
+ [
+ ([], "https://{wpt_host}"),
+ ([], "https://{wpt_host}/"),
+ ([], "https://{wpt_host}:1234/"),
+ ([], "https://{wpt_host}/path"),
+ ([], "https://{wpt_host}/?search"),
+ ([{},], "https://{wpt_host}"),
+ ([{},], "https://{wpt_host}/"),
+ ([{},], "https://{wpt_host}:1234/"),
+ ([{},], "https://{wpt_host}/path"),
+ ([{},], "https://{wpt_host}/?search"),
+ ([{"protocol": "https"},], "https://{wpt_host}/"),
+ ([{"protocol": "https"},], "https://{wpt_host}:1234/"),
+ ([{"protocol": "https"},], "https://{wpt_host}/path"),
+ ([{"protocol": "https"},], "https://{wpt_host}/?search"),
+ ([{"protocol": "HTTPS"},], "https://{wpt_host}/"),
+ ([{"hostname": "{wpt_host}"},], "https://{wpt_host}/"),
+ ([{"hostname": "{wpt_host}"},], "https://{wpt_host}:1234/"),
+ ([{"hostname": "{wpt_host}"},], "https://{wpt_host}/path"),
+ ([{"hostname": "{wpt_host}"},], "https://{wpt_host}/?search"),
+ ([{"hostname": "{wpt_host}"},], "https://{wpt_host_upper}/"),
+ ([{"hostname": "{wpt_host_upper}"},], "https://{wpt_host}/"),
+ ([{"port": "1234"},], "https://{wpt_host}:1234/"),
+ ([{"pathname": ""},], "https://{wpt_host}"),
+ ([{"pathname": ""},], "https://{wpt_host}/"),
+ ([{"pathname": "path"},], "https://{wpt_host}/path"),
+ ([{"search": ""},], "https://{wpt_host}/"),
+ ([{"search": ""},], "https://{wpt_host}/?"),
+ ([{"search": "search"},], "https://{wpt_host}/?search"),
+ ],
+)
+async def test_pattern_patterns_matching(
+ wait_for_event,
+ subscribe_events,
+ top_context,
+ add_intercept,
+ fetch,
+ substitute_host,
+ wait_for_future_safe,
+ patterns,
+ url_template,
+):
+ await subscribe_events(events=[BEFORE_REQUEST_SENT_EVENT], contexts=[top_context["context"]])
+
+ for pattern in patterns:
+ for key in pattern:
+ pattern[key] = substitute_host(pattern[key])
+
+ pattern.update({"type": "pattern"})
+
+ intercept = await add_intercept(phases=["beforeRequestSent"], url_patterns=patterns)
+
+ on_network_event = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ asyncio.ensure_future(fetch(substitute_host(url_template)))
+ event = await wait_for_future_safe(on_network_event)
+
+ assert_before_request_sent_event(event, is_blocked=True, intercepts=[intercept])
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "pattern, url_template",
+ [
+ ({"protocol": "http"}, "https://{wpt_host}/"),
+ ({"hostname": "abc.{wpt_host}"}, "https://{wpt_host}/"),
+ ({"hostname": "web-platform"}, "https://{wpt_host}/"),
+ ({"hostname": "web-platform.com"}, "https://{wpt_host}/"),
+ ({"port": "443"}, "https://{wpt_host}:1234/"),
+ ({"port": "1234"}, "https://{wpt_host}/"),
+ ({"pathname": ""}, "https://{wpt_host}/path"),
+ ({"pathname": "path"}, "https://{wpt_host}/"),
+ ({"pathname": "path"}, "https://{wpt_host}/path/"),
+ ({"pathname": "path"}, "https://{wpt_host}/other/path"),
+ ({"pathname": "path"}, "https://{wpt_host}/path/continued"),
+ ({"search": ""}, "https://{wpt_host}/?search"),
+ ({"search": "search"}, "https://{wpt_host}/?other"),
+ ],
+)
+async def test_pattern_patterns_not_matching(
+ wait_for_event,
+ subscribe_events,
+ top_context,
+ add_intercept,
+ fetch,
+ substitute_host,
+ wait_for_future_safe,
+ pattern,
+ url_template,
+):
+ await subscribe_events(events=[BEFORE_REQUEST_SENT_EVENT], contexts=[top_context["context"]])
+
+ for key in pattern:
+ pattern[key] = substitute_host(pattern[key])
+
+ pattern.update({"type": "pattern"})
+
+ await add_intercept(phases=["beforeRequestSent"], url_patterns=[pattern])
+
+ on_network_event = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ asyncio.ensure_future(fetch(substitute_host(url_template)))
+ event = await wait_for_future_safe(on_network_event)
+
+ assert_before_request_sent_event(event, is_blocked=False)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "pattern, url_template",
+ [
+ ("https://{wpt_host}/", "https://{wpt_host}/"),
+ ("https://{wpt_host}", "https://{wpt_host}/"),
+ ("https://{wpt_host}/", "https://{wpt_host}"),
+ ("HTTPS://{wpt_host}/", "https://{wpt_host}/"),
+ ("https://{wpt_host}/", "HTTPS://{wpt_host}/"),
+ ("https://{wpt_host_upper}/", "https://{wpt_host}/"),
+ ("https://{wpt_host}/", "https://{wpt_host_upper}/"),
+ ("https://user:password@{wpt_host}/", "https://{wpt_host}/"),
+ ("https://{wpt_host}/", "https://{wpt_host}:443/"),
+ ("https://{wpt_host}:443/", "https://{wpt_host}/"),
+ ("https://{wpt_host}:443/", "https://{wpt_host}:443/"),
+ ("https://{wpt_host}:1234/", "https://{wpt_host}:1234/"),
+ ("https://{wpt_host}/path", "https://{wpt_host}/path"),
+ ("https://{wpt_host}/?search", "https://{wpt_host}/?search"),
+ ("https://{wpt_host}/#ref", "https://{wpt_host}/"),
+ ("https://{wpt_host}/", "https://{wpt_host}/#ref"),
+ ("https://{wpt_host}/#ref1", "https://{wpt_host}/#ref2"),
+ ],
+)
+async def test_string_patterns_matching(
+ wait_for_event,
+ subscribe_events,
+ top_context,
+ add_intercept,
+ fetch,
+ substitute_host,
+ wait_for_future_safe,
+ pattern,
+ url_template,
+):
+ await subscribe_events(events=[BEFORE_REQUEST_SENT_EVENT], contexts=[top_context["context"]])
+
+ intercept = await add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "string", "pattern": substitute_host(pattern)}],
+ )
+
+ on_network_event = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ asyncio.ensure_future(fetch(substitute_host(url_template)))
+ event = await wait_for_future_safe(on_network_event)
+
+ assert_before_request_sent_event(event, is_blocked=True, intercepts=[intercept])
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "pattern, url_template",
+ [
+ ("https://{wpt_host}/", "https://some.other.host/"),
+ ("https://{wpt_host}:1234/", "https://{wpt_host}:5678/"),
+ ("https://{wpt_host}/", "https://{wpt_host}:5678/"),
+ ("https://{wpt_host}/path", "https://{wpt_host}/other/path"),
+ ("https://{wpt_host}/path", "https://{wpt_host}/path/continued"),
+ ("https://{wpt_host}/pathcase", "https://{wpt_host}/PATHCASE"),
+ ("https://{wpt_host}/?searchcase", "https://{wpt_host}/?SEARCHCASE"),
+ ("https://{wpt_host}/?key", "https://{wpt_host}/?otherkey"),
+ ("https://{wpt_host}/?key", "https://{wpt_host}/?key=value"),
+ ("https://{wpt_host}/?a=b&c=d", "https://{wpt_host}/?c=d&a=b"),
+ ("https://{wpt_host}/??", "https://{wpt_host}/?"),
+ ],
+)
+async def test_string_patterns_not_matching(
+ wait_for_event,
+ subscribe_events,
+ top_context,
+ add_intercept,
+ fetch,
+ substitute_host,
+ wait_for_future_safe,
+ pattern,
+ url_template,
+):
+ await subscribe_events(events=[BEFORE_REQUEST_SENT_EVENT], contexts=[top_context["context"]])
+
+ await add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "string", "pattern": substitute_host(pattern)}],
+ )
+
+ on_network_event = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ asyncio.ensure_future(fetch(substitute_host(url_template)))
+ event = await wait_for_future_safe(on_network_event)
+
+ assert_before_request_sent_event(event, is_blocked=False)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/auth_required.py b/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/auth_required.py
new file mode 100644
index 0000000000..9a24946cde
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/auth_required.py
@@ -0,0 +1,76 @@
+import pytest
+
+from .. import assert_response_event, AUTH_REQUIRED_EVENT, PAGE_EMPTY_HTML
+
+
+@pytest.mark.asyncio
+async def test_subscribe_status(
+ bidi_session, new_tab, subscribe_events, wait_for_event, wait_for_future_safe, url
+):
+ await subscribe_events(events=[AUTH_REQUIRED_EVENT])
+
+ # Track all received network.authRequired events in the events array.
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(AUTH_REQUIRED_EVENT, on_event)
+
+ auth_url = url(
+ "/webdriver/tests/support/http_handlers/authentication.py?realm=testrealm"
+ )
+
+ on_auth_required = wait_for_event(AUTH_REQUIRED_EVENT)
+
+ # navigate using wait="none" as other wait conditions would hang because of
+ # the authentication prompt.
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=auth_url,
+ wait="none",
+ )
+
+ await wait_for_future_safe(on_auth_required)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": auth_url}
+ expected_response = {
+ "url": auth_url,
+ "authChallenges": [
+ ({"scheme": "Basic", "realm": "testrealm"}),
+ ],
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_no_authentication(
+ bidi_session, new_tab, subscribe_events, url
+):
+ await subscribe_events(events=[AUTH_REQUIRED_EVENT])
+
+ # Track all received network.authRequired events in the events array.
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(AUTH_REQUIRED_EVENT, on_event)
+
+ # Navigate to a page which should not trigger any authentication.
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url(PAGE_EMPTY_HTML),
+ wait="complete",
+ )
+
+ assert len(events) == 0
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/unsubscribe.py b/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/unsubscribe.py
new file mode 100644
index 0000000000..cf818fee6f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/auth_required/unsubscribe.py
@@ -0,0 +1,35 @@
+import asyncio
+
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+from .. import AUTH_REQUIRED_EVENT, PAGE_EMPTY_HTML
+
+
+# This test can be moved back to `auth_required.py` when all implementations
+# support handing of HTTP auth prompt.
+async def test_unsubscribe(bidi_session, new_tab, url):
+ await bidi_session.session.subscribe(events=[AUTH_REQUIRED_EVENT])
+ await bidi_session.session.unsubscribe(events=[AUTH_REQUIRED_EVENT])
+
+ # Track all received network.authRequired events in the events array.
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(AUTH_REQUIRED_EVENT, on_event)
+
+ # Navigate to authentication.py again and check no event is received.
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url(
+ "/webdriver/tests/support/http_handlers/authentication.py?realm=testrealm"
+ ),
+ wait="none",
+ )
+ await asyncio.sleep(0.5)
+ assert len(events) == 0
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/before_request_sent.py b/testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/before_request_sent.py
new file mode 100644
index 0000000000..c92337e507
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/before_request_sent/before_request_sent.py
@@ -0,0 +1,395 @@
+import asyncio
+
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.sync import AsyncPoll
+
+from .. import (
+ assert_before_request_sent_event,
+ PAGE_EMPTY_HTML,
+ PAGE_EMPTY_TEXT,
+ PAGE_REDIRECT_HTTP_EQUIV,
+ PAGE_REDIRECTED_HTML,
+ BEFORE_REQUEST_SENT_EVENT,
+)
+
+
+@pytest.mark.asyncio
+async def test_subscribe_status(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe, url, fetch):
+ await subscribe_events(events=[BEFORE_REQUEST_SENT_EVENT])
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url(PAGE_EMPTY_HTML),
+ wait="complete",
+ )
+
+ # Track all received network.beforeRequestSent events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ BEFORE_REQUEST_SENT_EVENT, on_event
+ )
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ await fetch(text_url)
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": text_url}
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+ await bidi_session.session.unsubscribe(events=[BEFORE_REQUEST_SENT_EVENT])
+
+ # Fetch the text url again, with an additional parameter to bypass the cache
+ # and check no new event is received.
+ await fetch(f"{text_url}?nocache")
+ await asyncio.sleep(0.5)
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_iframe_load(
+ bidi_session,
+ top_context,
+ setup_network_test,
+ test_page,
+ test_page_same_origin_frame,
+):
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=test_page_same_origin_frame,
+ wait="complete",
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context = contexts[0]["children"][0]
+
+ assert len(events) == 2
+ assert_before_request_sent_event(
+ events[0],
+ expected_request={"url": test_page_same_origin_frame},
+ context=top_context["context"],
+ )
+ assert_before_request_sent_event(
+ events[1],
+ expected_request={"url": test_page},
+ context=frame_context["context"],
+ )
+
+
+@pytest.mark.asyncio
+async def test_load_page_twice(
+ bidi_session, top_context, wait_for_event, url, setup_network_test, wait_for_future_safe
+):
+ html_url = url(PAGE_EMPTY_HTML)
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=html_url,
+ wait="complete",
+ )
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": html_url}
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_navigation_id(
+ bidi_session, top_context, wait_for_event, url, fetch, setup_network_test, wait_for_future_safe
+):
+ html_url = url(PAGE_EMPTY_HTML)
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=html_url,
+ wait="complete",
+ )
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": html_url}
+ assert_before_request_sent_event(
+ events[0], expected_request=expected_request, navigation=result["navigation"]
+ )
+ assert events[0]["navigation"] is not None
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ await fetch(text_url, method="GET")
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 2
+ expected_request = {"method": "GET", "url": text_url}
+ assert_before_request_sent_event(
+ events[1],
+ expected_request=expected_request,
+ )
+ # Check that requests not related to a navigation have no navigation id.
+ assert events[1]["navigation"] is None
+
+
+@pytest.mark.parametrize(
+ "method",
+ [
+ "GET",
+ "HEAD",
+ "POST",
+ "PUT",
+ "DELETE",
+ "OPTIONS",
+ "PATCH",
+ ],
+)
+@pytest.mark.asyncio
+async def test_request_method(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test, method
+):
+ text_url = url(PAGE_EMPTY_TEXT)
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ await fetch(text_url, method=method)
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 1
+ expected_request = {"method": method, "url": text_url}
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_request_headers(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test
+):
+ text_url = url(PAGE_EMPTY_TEXT)
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ await fetch(text_url, method="GET", headers={"foo": "bar"})
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 1
+ expected_request = {
+ "headers": ({"name": "foo", "value": {"type": "string", "value": "bar"}},),
+ "method": "GET",
+ "url": text_url,
+ }
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_request_cookies(
+ bidi_session, top_context, wait_for_event, wait_for_future_safe, url, fetch, setup_network_test
+):
+ text_url = url(PAGE_EMPTY_TEXT)
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ await bidi_session.script.evaluate(
+ expression="document.cookie = 'foo=bar';",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ await fetch(text_url, method="GET")
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 1
+ expected_request = {
+ "cookies": ({"name": "foo", "value": {"type": "string", "value": "bar"}},),
+ "method": "GET",
+ "url": text_url,
+ }
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+ await bidi_session.script.evaluate(
+ expression="document.cookie = 'fuu=baz';",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ on_before_request_sent = wait_for_event(BEFORE_REQUEST_SENT_EVENT)
+ await fetch(text_url, method="GET")
+ await wait_for_future_safe(on_before_request_sent)
+
+ assert len(events) == 2
+
+ expected_request = {
+ "cookies": (
+ {"name": "foo", "value": {"type": "string", "value": "bar"}},
+ {"name": "fuu", "value": {"type": "string", "value": "baz"}},
+ ),
+ "method": "GET",
+ "url": text_url,
+ }
+ assert_before_request_sent_event(
+ events[1],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_redirect(bidi_session, wait_for_event, url, fetch, setup_network_test):
+ text_url = url(PAGE_EMPTY_TEXT)
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={text_url}"
+ )
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ await fetch(redirect_url, method="GET")
+
+ # Wait until we receive two events, one for the initial request and one for
+ # the redirection.
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+
+ assert len(events) == 2
+ expected_request = {"method": "GET", "url": redirect_url}
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+ expected_request = {"method": "GET", "url": text_url}
+ assert_before_request_sent_event(
+ events[1], expected_request=expected_request, redirect_count=1
+ )
+
+ # Check that both requests share the same requestId
+ assert events[0]["request"]["request"] == events[1]["request"]["request"]
+
+
+@pytest.mark.asyncio
+async def test_redirect_http_equiv(
+ bidi_session, top_context, wait_for_event, url, setup_network_test
+):
+ # PAGE_REDIRECT_HTTP_EQUIV should redirect to PAGE_REDIRECTED_HTML immediately
+ http_equiv_url = url(PAGE_REDIRECT_HTTP_EQUIV)
+ redirected_url = url(PAGE_REDIRECTED_HTML)
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=http_equiv_url,
+ wait="complete",
+ )
+
+ # Wait until we receive two events, one for the initial request and one for
+ # the http-equiv "redirect".
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+
+ assert len(events) == 2
+ expected_request = {"method": "GET", "url": http_equiv_url}
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ navigation=result["navigation"],
+ )
+ # http-equiv redirect should not be considered as a redirect: redirect_count
+ # should be 0.
+ expected_request = {"method": "GET", "url": redirected_url}
+ assert_before_request_sent_event(
+ events[1],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+ # Check that the http-equiv redirect request has a different requestId
+ assert events[0]["request"]["request"] != events[1]["request"]["request"]
+
+ # Check that the http-equiv redirect request also has a navigation id set,
+ # but different from the original request.
+ assert events[1]["navigation"] is not None
+ assert events[1]["navigation"] != events[0]["navigation"]
+
+
+@pytest.mark.asyncio
+async def test_redirect_navigation(
+ bidi_session, top_context, wait_for_event, url, setup_network_test
+):
+ html_url = url(PAGE_EMPTY_HTML)
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={html_url}"
+ )
+
+ network_events = await setup_network_test(events=[BEFORE_REQUEST_SENT_EVENT])
+ events = network_events[BEFORE_REQUEST_SENT_EVENT]
+
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=redirect_url,
+ wait="complete",
+ )
+
+ assert len(events) == 2
+ expected_request = {"method": "GET", "url": redirect_url}
+ assert_before_request_sent_event(
+ events[0],
+ expected_request=expected_request,
+ navigation=result["navigation"],
+ redirect_count=0,
+ )
+ expected_request = {"method": "GET", "url": html_url}
+ assert_before_request_sent_event(
+ events[1],
+ expected_request=expected_request,
+ navigation=result["navigation"],
+ redirect_count=1,
+ )
+
+ # Check that both requests share the same requestId
+ assert events[0]["request"]["request"] == events[1]["request"]["request"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/combined/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/combined/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/combined/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/combined/network_events.py b/testing/web-platform/tests/webdriver/tests/bidi/network/combined/network_events.py
new file mode 100644
index 0000000000..7b6d99727a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/combined/network_events.py
@@ -0,0 +1,269 @@
+import asyncio
+
+import pytest
+
+from .. import (
+ assert_before_request_sent_event,
+ assert_response_event,
+ PAGE_EMPTY_HTML,
+ PAGE_EMPTY_TEXT,
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ RESPONSE_STARTED_EVENT,
+)
+
+
+@pytest.mark.asyncio
+async def test_iframe_navigation_request(
+ bidi_session,
+ top_context,
+ subscribe_events,
+ setup_network_test,
+ inline,
+ test_page,
+ test_page_cross_origin,
+ test_page_same_origin_frame,
+):
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ],
+ contexts=[top_context["context"]],
+ )
+
+ navigation_events = []
+
+ async def on_event(method, data):
+ navigation_events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ "browsingContext.navigationStarted", on_event
+ )
+ await subscribe_events(events=["browsingContext.navigationStarted"])
+
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_same_origin_frame, wait="complete"
+ )
+
+ # Get the frame_context loaded in top_context
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ assert len(contexts[0]["children"]) == 1
+ frame_context = contexts[0]["children"][0]
+
+ assert len(navigation_events) == 2
+ assert len(network_events[BEFORE_REQUEST_SENT_EVENT]) == 2
+ assert len(network_events[RESPONSE_STARTED_EVENT]) == 2
+ assert len(network_events[RESPONSE_COMPLETED_EVENT]) == 2
+
+ # Check that 2 distinct navigations were captured, for the expected contexts
+ assert navigation_events[0]["navigation"] == result["navigation"]
+ assert navigation_events[0]["context"] == top_context["context"]
+ assert navigation_events[1]["navigation"] != result["navigation"]
+ assert navigation_events[1]["context"] == frame_context["context"]
+
+ # Helper to assert the 3 main network events for this test
+ def assert_events(event_index, url, context, navigation):
+ expected_request = {"method": "GET", "url": url}
+ expected_response = {"url": url}
+ assert_before_request_sent_event(
+ network_events[BEFORE_REQUEST_SENT_EVENT][event_index],
+ expected_request=expected_request,
+ context=context,
+ navigation=navigation,
+ )
+ assert_response_event(
+ network_events[RESPONSE_STARTED_EVENT][event_index],
+ expected_response=expected_response,
+ context=context,
+ navigation=navigation,
+ )
+ assert_response_event(
+ network_events[RESPONSE_COMPLETED_EVENT][event_index],
+ expected_response=expected_response,
+ context=context,
+ navigation=navigation,
+ )
+
+ assert_events(
+ 0,
+ url=test_page_same_origin_frame,
+ context=top_context["context"],
+ navigation=navigation_events[0]["navigation"],
+ )
+ assert_events(
+ 1,
+ url=test_page,
+ context=frame_context["context"],
+ navigation=navigation_events[1]["navigation"],
+ )
+
+ # Navigate the iframe to another url
+ result = await bidi_session.browsing_context.navigate(
+ context=frame_context["context"], url=test_page_cross_origin, wait="complete"
+ )
+
+ assert len(navigation_events) == 3
+ assert len(network_events[BEFORE_REQUEST_SENT_EVENT]) == 3
+ assert len(network_events[RESPONSE_STARTED_EVENT]) == 3
+ assert len(network_events[RESPONSE_COMPLETED_EVENT]) == 3
+ assert_events(
+ 2,
+ url=test_page_cross_origin,
+ context=frame_context["context"],
+ navigation=navigation_events[2]["navigation"],
+ )
+
+
+@pytest.mark.asyncio
+async def test_same_navigation_id(
+ bidi_session, top_context, wait_for_event, wait_for_future_safe, url, setup_network_test
+):
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ],
+ contexts=[top_context["context"]],
+ )
+
+ html_url = url(PAGE_EMPTY_HTML)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=html_url,
+ wait="complete",
+ )
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(network_events[BEFORE_REQUEST_SENT_EVENT]) == 1
+ assert len(network_events[RESPONSE_STARTED_EVENT]) == 1
+ assert len(network_events[RESPONSE_COMPLETED_EVENT]) == 1
+ expected_request = {"method": "GET", "url": html_url}
+ expected_response = {"url": html_url}
+ assert_before_request_sent_event(
+ network_events[BEFORE_REQUEST_SENT_EVENT][0],
+ expected_request=expected_request,
+ context=top_context["context"],
+ navigation=result["navigation"],
+ )
+ assert_response_event(
+ network_events[RESPONSE_STARTED_EVENT][0],
+ expected_response=expected_response,
+ context=top_context["context"],
+ navigation=result["navigation"],
+ )
+ assert_response_event(
+ network_events[RESPONSE_COMPLETED_EVENT][0],
+ expected_response=expected_response,
+ context=top_context["context"],
+ navigation=result["navigation"],
+ )
+
+
+@pytest.mark.asyncio
+async def test_same_request_id(wait_for_event, wait_for_future_safe, url, setup_network_test, fetch):
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ before_request_sent_events = network_events[BEFORE_REQUEST_SENT_EVENT]
+ response_started_events = network_events[RESPONSE_STARTED_EVENT]
+ response_completed_events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(text_url)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(before_request_sent_events) == 1
+ assert len(response_started_events) == 1
+ assert len(response_completed_events) == 1
+ expected_request = {"method": "GET", "url": text_url}
+ assert_before_request_sent_event(
+ before_request_sent_events[0], expected_request=expected_request
+ )
+
+ expected_response = {"url": text_url}
+ assert_response_event(
+ response_started_events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+ assert_response_event(
+ response_completed_events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ assert (
+ before_request_sent_events[0]["request"]["request"] == response_started_events[0]["request"]["request"]
+ )
+
+ assert (
+ before_request_sent_events[0]["request"]["request"] == response_completed_events[0]["request"]["request"]
+ )
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_one_context(
+ bidi_session, top_context, wait_for_event, wait_for_future_safe, url, fetch, setup_network_test
+):
+ other_context = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.browsing_context.navigate(
+ context=other_context["context"],
+ url=url(PAGE_EMPTY_HTML),
+ wait="complete",
+ )
+
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ],
+ contexts=[top_context["context"]],
+ )
+
+ # Perform a fetch request in the subscribed context and wait for the response completed event.
+ text_url = url(PAGE_EMPTY_TEXT)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(text_url, context=top_context)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(network_events[BEFORE_REQUEST_SENT_EVENT]) == 1
+ assert len(network_events[RESPONSE_STARTED_EVENT]) == 1
+ assert len(network_events[RESPONSE_COMPLETED_EVENT]) == 1
+
+ # Check the received events have the correct context.
+ expected_request = {"method": "GET", "url": text_url}
+ expected_response = {"url": text_url}
+ assert_before_request_sent_event(
+ network_events[BEFORE_REQUEST_SENT_EVENT][0],
+ expected_request=expected_request,
+ context=top_context["context"],
+ )
+ assert_response_event(
+ network_events[RESPONSE_STARTED_EVENT][0],
+ expected_response=expected_response,
+ context=top_context["context"],
+ )
+ assert_response_event(
+ network_events[RESPONSE_COMPLETED_EVENT][0],
+ expected_response=expected_response,
+ context=top_context["context"],
+ )
+
+ # Perform another fetch request in the other context.
+ await fetch(text_url, context=other_context)
+ await asyncio.sleep(0.5)
+
+ # Check that no other event was received.
+ assert len(network_events[BEFORE_REQUEST_SENT_EVENT]) == 1
+ assert len(network_events[RESPONSE_STARTED_EVENT]) == 1
+ assert len(network_events[RESPONSE_COMPLETED_EVENT]) == 1
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/network/conftest.py
new file mode 100644
index 0000000000..934b649c91
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/conftest.py
@@ -0,0 +1,206 @@
+import json
+
+import asyncio
+import pytest
+import pytest_asyncio
+
+from webdriver.bidi.error import NoSuchInterceptException
+from webdriver.bidi.modules.script import ContextTarget
+
+from . import PAGE_EMPTY_HTML, PAGE_EMPTY_TEXT, RESPONSE_COMPLETED_EVENT
+
+
+@pytest_asyncio.fixture
+async def add_intercept(bidi_session):
+ """Add a network intercept for the provided phases and url patterns, and
+ ensure the intercept is removed at the end of the test."""
+
+ intercepts = []
+
+ async def add_intercept(phases, url_patterns):
+ nonlocal intercepts
+ intercept = await bidi_session.network.add_intercept(
+ phases=phases,
+ url_patterns=url_patterns,
+ )
+ intercepts.append(intercept)
+
+ return intercept
+
+ yield add_intercept
+
+ # Remove all added intercepts at the end of the test
+ for intercept in intercepts:
+ try:
+ await bidi_session.network.remove_intercept(intercept=intercept)
+ except NoSuchInterceptException:
+ # Ignore exceptions in case a specific intercept was already removed
+ # during the test.
+ pass
+
+
+@pytest.fixture
+def fetch(bidi_session, top_context, configuration):
+ """Perform a fetch from the page of the provided context, default to the
+ top context.
+ """
+
+ async def fetch(
+ url, method="GET", headers=None, context=top_context, timeout_in_seconds=3
+ ):
+ method_arg = f"method: '{method}',"
+
+ headers_arg = ""
+ if headers is not None:
+ headers_arg = f"headers: {json.dumps(headers)},"
+
+ timeout_in_seconds = timeout_in_seconds * configuration["timeout_multiplier"]
+
+ # Wait for fetch() to resolve a response and for response.text() to
+ # resolve as well to make sure the request/response is completed when
+ # the helper returns.
+ await bidi_session.script.evaluate(
+ expression=f"""
+ {{
+ const controller = new AbortController();
+ setTimeout(() => controller.abort(), {timeout_in_seconds * 1000});
+ fetch("{url}", {{
+ {method_arg}
+ {headers_arg}
+ signal: controller.signal
+ }}).then(response => response.text());
+ }}""",
+ target=ContextTarget(context["context"]),
+ await_promise=True,
+ )
+
+ return fetch
+
+
+@pytest_asyncio.fixture
+async def setup_network_test(
+ bidi_session,
+ subscribe_events,
+ wait_for_event,
+ wait_for_future_safe,
+ top_context,
+ url,
+):
+ """Navigate the current top level context to the provided url and subscribe
+ to network.beforeRequestSent.
+
+ Returns an `events` dictionary in which the captured network events will be added.
+ The keys of the dictionary are network event names (eg. "network.beforeRequestSent"),
+ and the value is an array of collected events.
+ """
+ listeners = []
+
+ async def _setup_network_test(events, test_url=url(PAGE_EMPTY_HTML), contexts=None):
+ nonlocal listeners
+
+ # Listen for network.responseCompleted for the initial navigation to
+ # make sure this event will not be captured unexpectedly by the tests.
+ await bidi_session.session.subscribe(
+ events=[RESPONSE_COMPLETED_EVENT], contexts=[top_context["context"]]
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=test_url,
+ wait="complete",
+ )
+ await wait_for_future_safe(on_response_completed)
+ await bidi_session.session.unsubscribe(
+ events=[RESPONSE_COMPLETED_EVENT], contexts=[top_context["context"]]
+ )
+
+ await subscribe_events(events, contexts)
+
+ network_events = {}
+ for event in events:
+ network_events[event] = []
+
+ async def on_event(method, data, event=event):
+ network_events[event].append(data)
+
+ listeners.append(bidi_session.add_event_listener(event, on_event))
+
+ return network_events
+
+ yield _setup_network_test
+
+ # cleanup
+ for remove_listener in listeners:
+ remove_listener()
+
+
+@pytest_asyncio.fixture
+async def setup_blocked_request(
+ bidi_session,
+ setup_network_test,
+ url,
+ add_intercept,
+ fetch,
+ wait_for_event,
+ top_context,
+):
+ """Creates an intercept for the provided phase, sends a fetch request that
+ should be blocked by this intercept and resolves when the corresponding
+ event is received. Pass navigate=True in order to navigate instead of doing
+ a fetch request.
+
+ For the "authRequired" phase, the request will be sent to the authentication
+ http handler. The optional arguments username, password and realm can be used
+ to configure the handler.
+
+ Returns the `request` id of the intercepted request.
+ """
+
+ async def setup_blocked_request(
+ phase,
+ context=top_context,
+ username="user",
+ password="password",
+ realm="test",
+ navigate=False,
+ ):
+ await setup_network_test(events=[f"network.{phase}"])
+
+ if phase == "authRequired":
+ blocked_url = url(
+ "/webdriver/tests/support/http_handlers/authentication.py?"
+ f"username={username}&password={password}&realm={realm}"
+ )
+ if navigate:
+ # By default the authentication handler returns a text/plain
+ # content-type. Switch to text/html for a regular navigation.
+ blocked_url = f"{blocked_url}&contenttype=text/html"
+ else:
+ blocked_url = url(PAGE_EMPTY_TEXT)
+
+ await add_intercept(
+ phases=[phase],
+ url_patterns=[
+ {
+ "type": "string",
+ "pattern": blocked_url,
+ }
+ ],
+ )
+
+ if navigate:
+ asyncio.ensure_future(
+ bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=blocked_url, wait="complete"
+ )
+ )
+ else:
+ asyncio.ensure_future(fetch(blocked_url))
+
+ event = await wait_for_event(f"network.{phase}")
+ request = event["request"]["request"]
+
+ return request
+
+ return setup_blocked_request
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/invalid.py
new file mode 100644
index 0000000000..d7cfa629c0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/invalid.py
@@ -0,0 +1,318 @@
+# META: timeout=long
+
+import pytest
+import webdriver.bidi.error as error
+
+from .. import (
+ create_cookie_header,
+ create_header,
+ PAGE_EMPTY_TEXT,
+ RESPONSE_COMPLETED_EVENT,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_body_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, body=value)
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_body_invalid_value(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, body=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_body_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request, body={"type": value, "value": "foo"}
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_body_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request, body={"type": value, "value": "foo"}
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_body_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request, body={"type": "string", "value": value}
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", {}])
+async def test_params_cookies_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, cookies=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_cookies_cookie_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, cookies=[value])
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_name_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ cookies=[create_cookie_header(overrides={"name": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_cookies_cookie_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ cookies=[create_cookie_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_cookies_cookie_value_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ cookies=[create_cookie_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_value_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_cookies_cookie_value_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_value_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", {}])
+async def test_params_headers_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, headers=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_headers_header_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, headers=[value])
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_name_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ headers=[create_header(overrides={"name": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_headers_header_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ headers=[create_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_headers_header_value_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ headers=[create_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_value_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ headers=[create_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_headers_header_value_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ headers=[create_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_value_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request,
+ headers=[create_header(value_overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_method_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, method=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_request_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=value)
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_request_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_request(request=value)
+
+
+async def test_params_request_no_such_request(
+ bidi_session, setup_network_test, wait_for_event, fetch, url
+):
+ await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ await fetch(text_url)
+
+ response_completed_event = await on_response_completed
+ request = response_completed_event["request"]["request"]
+
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_request(request=request)
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_url_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(request=request, url=value)
+
+
+@pytest.mark.parametrize("protocol", ["http", "https"])
+@pytest.mark.parametrize("value", [":invalid", "#invalid"])
+async def test_params_url_invalid_value(
+ setup_blocked_request, bidi_session, protocol, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_request(
+ request=request, url=f"{protocol}://{value}"
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/request.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/request.py
new file mode 100644
index 0000000000..c55e477ad7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_request/request.py
@@ -0,0 +1,50 @@
+import pytest
+
+from .. import RESPONSE_COMPLETED_EVENT, RESPONSE_STARTED_EVENT
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_continue_fetch_request(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ await subscribe_events(
+ events=[
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ await bidi_session.network.continue_request(request=request)
+
+ await on_response_started
+ await on_response_completed
+
+
+async def test_continue_navigation(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session
+):
+ request = await setup_blocked_request("beforeRequestSent", navigate=True)
+
+ await subscribe_events(
+ events=[
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ "browsingContext.load",
+ ]
+ )
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ on_load = wait_for_event("browsingContext.load")
+
+ await bidi_session.network.continue_request(request=request)
+
+ await on_response_started
+ await on_response_completed
+ await on_load
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/credentials.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/credentials.py
new file mode 100644
index 0000000000..3e595722cc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/credentials.py
@@ -0,0 +1,78 @@
+import pytest
+
+from webdriver.bidi.modules.network import AuthCredentials
+
+from tests.support.sync import AsyncPoll
+
+from .. import AUTH_REQUIRED_EVENT, RESPONSE_COMPLETED_EVENT
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("navigate", [False, True], ids=["fetch", "navigate"])
+async def test_wrong_credentials(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, navigate
+):
+ username = f"test_missing_credentials_{navigate}"
+ password = f"test_missing_credentials_password_{navigate}"
+ request = await setup_blocked_request(
+ "authRequired", username=username, password=password, navigate=navigate
+ )
+
+ await subscribe_events(events=[AUTH_REQUIRED_EVENT])
+
+ # Continue the request blocked on authRequired, with incorrect credentials.
+ on_auth_required = wait_for_event(AUTH_REQUIRED_EVENT)
+ wrong_credentials = AuthCredentials(username=username, password="wrong_password")
+ await bidi_session.network.continue_response(
+ request=request, credentials=wrong_credentials
+ )
+ await on_auth_required
+
+
+@pytest.mark.parametrize("navigate", [False, True], ids=["fetch", "navigate"])
+async def test_correct_credentials(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, navigate
+):
+ # Setup unique username / password because browsers cache credentials.
+ username = f"test_wrong_credentials_{navigate}"
+ password = f"test_wrong_credentials_password_{navigate}"
+ request = await setup_blocked_request(
+ "authRequired", username=username, password=password, navigate=navigate
+ )
+
+ await subscribe_events(
+ events=[AUTH_REQUIRED_EVENT, RESPONSE_COMPLETED_EVENT, "browsingContext.load"]
+ )
+
+ # Track all network.responseCompleted events.
+ response_completed_events = []
+
+ async def on_event(method, data):
+ response_completed_events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ RESPONSE_COMPLETED_EVENT, on_event
+ )
+
+ # Continue with the expected credentials.
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ if navigate:
+ on_load = wait_for_event("browsingContext.load")
+
+ correct_credentials = AuthCredentials(username=username, password=password)
+ await bidi_session.network.continue_response(
+ request=request, credentials=correct_credentials
+ )
+ await on_response_completed
+ if navigate:
+ await on_load
+
+ # Wait until 2 responseCompleted events have been emitted:
+ # - one for the initial request
+ # - one for the continue with correct credentials
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(response_completed_events) >= 2)
+ assert len(response_completed_events) == 2
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/invalid.py
new file mode 100644
index 0000000000..41f786ef6d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/invalid.py
@@ -0,0 +1,455 @@
+# META: timeout=long
+
+import pytest
+import webdriver.bidi.error as error
+
+from .. import (
+ create_cookie_header,
+ create_header,
+ PAGE_EMPTY_TEXT,
+ RESPONSE_COMPLETED_EVENT,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", {}])
+async def test_params_cookies_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, cookies=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_cookies_cookie_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, cookies=[value])
+
+
+@pytest.mark.parametrize(
+ "value",
+ [{}, {"name": "name"}, {"value": {"type": "string", "value": "foo"}}],
+ ids=[
+ "empty object",
+ "missing value",
+ "missing name",
+ ],
+)
+async def test_params_cookies_cookie_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[value],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_name_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"name": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_cookies_cookie_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_cookies_cookie_value_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_value_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_cookies_cookie_value_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("property", ["domain", "expiry", "path", "sameSite"])
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_cookies_cookie_value_string_properties_invalid_type(
+ setup_blocked_request, bidi_session, property, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={property: value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_cookies_cookie_value_same_site_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"sameSite": value})],
+ )
+
+
+@pytest.mark.parametrize("property", ["httpOnly", "secure"])
+@pytest.mark.parametrize("value", [42, "foo", {}, []])
+async def test_params_cookies_cookie_value_bool_properties_invalid_type(
+ setup_blocked_request, bidi_session, property, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={property: value})],
+ )
+
+
+@pytest.mark.parametrize("value", [False, "foo", {}, []])
+async def test_params_cookies_cookie_value_max_age_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"maxAge": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [4.3])
+async def test_params_cookies_cookie_value_max_age_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"maxAge": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_value_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_credentials_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, credentials=value)
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ {"type": "password", "password": "foo"},
+ {"type": "password", "username": "foo"},
+ {
+ "type": "password",
+ },
+ {
+ "username": "foo",
+ "password": "bar",
+ },
+ ],
+ ids=[
+ "missing username",
+ "missing password",
+ "missing username and password",
+ "missing type",
+ ],
+)
+async def test_params_credentials_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, credentials=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_credentials_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ credentials={
+ "type": value,
+ },
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_credentials_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ credentials={
+ "type": value,
+ },
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_credentials_username_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+ credentials = {"type": "password", "username": value, "password": "foo"}
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request, credentials=credentials
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_credentials_password_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+ credentials = {"type": "password", "username": "foo", "password": value}
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request, credentials=credentials
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", {}])
+async def test_params_headers_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, headers=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_headers_header_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, headers=[value])
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_name_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ headers=[create_header(overrides={"name": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_headers_header_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ headers=[create_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_headers_header_value_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ headers=[create_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_value_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ headers=[create_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_headers_header_value_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ headers=[create_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_value_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request,
+ headers=[create_header(value_overrides={"value": value})],
+ )
+
+
+async def test_params_request_invalid_phase(setup_blocked_request, bidi_session):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_request_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=value)
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_request_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_response(request=value)
+
+
+async def test_params_request_no_such_request(
+ bidi_session, setup_network_test, wait_for_event, fetch, url
+):
+ await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ await fetch(text_url)
+
+ response_completed_event = await on_response_completed
+ request = response_completed_event["request"]["request"]
+
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_response(request=request)
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_reason_phrase_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(
+ request=request, reason_phrase=value
+ )
+
+
+@pytest.mark.parametrize("value", [False, "foo", {}, []])
+async def test_params_status_code_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, status_code=value)
+
+
+@pytest.mark.parametrize("value", [-1, 4.3])
+async def test_params_status_code_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("responseStarted")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_response(request=request, status_code=value)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/request.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/request.py
new file mode 100644
index 0000000000..579f1da288
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_response/request.py
@@ -0,0 +1,57 @@
+import pytest
+
+from webdriver.bidi.modules.network import AuthCredentials
+
+from tests.support.sync import AsyncPoll
+
+from .. import AUTH_REQUIRED_EVENT, RESPONSE_COMPLETED_EVENT, RESPONSE_STARTED_EVENT
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("navigate", [False, True], ids=["fetch", "navigate"])
+async def test_continue_auth_required(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, navigate
+):
+ # Setup unique username / password because browsers cache credentials.
+ username = f"test_continue_auth_required_{navigate}"
+ password = f"test_continue_auth_required_password_{navigate}"
+ request = await setup_blocked_request(
+ "authRequired", username=username, password=password, navigate=navigate
+ )
+
+ await subscribe_events(
+ events=[
+ AUTH_REQUIRED_EVENT,
+ ]
+ )
+
+ # Continue the request blocked on authRequired. Without credentials, another
+ # network.authRequired should be emitted.
+ on_auth_required = wait_for_event(AUTH_REQUIRED_EVENT)
+ await bidi_session.network.continue_response(request=request)
+ await on_auth_required
+
+
+@pytest.mark.parametrize("navigate", [False, True], ids=["fetch", "navigate"])
+async def test_continue_response_started(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, navigate
+):
+ request = await setup_blocked_request("responseStarted", navigate=navigate)
+
+ await subscribe_events(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ "browsingContext.load",
+ ]
+ )
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ if navigate:
+ on_load = wait_for_event("browsingContext.load")
+
+ await bidi_session.network.continue_response(request=request)
+
+ await on_response_completed
+ if navigate:
+ await on_load
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/action.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/action.py
new file mode 100644
index 0000000000..a122ce0e49
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/action.py
@@ -0,0 +1,148 @@
+import pytest
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.network import AuthCredentials
+from webdriver.error import TimeoutException
+
+from tests.support.sync import AsyncPoll
+from .. import (
+ assert_response_event,
+ AUTH_REQUIRED_EVENT,
+ PAGE_EMPTY_TEXT,
+ RESPONSE_COMPLETED_EVENT,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_cancel(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, url
+):
+ request = await setup_blocked_request("authRequired")
+ await subscribe_events(events=[RESPONSE_COMPLETED_EVENT])
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await bidi_session.network.continue_with_auth(request=request, action="cancel")
+ await on_response_completed
+
+ response_event = await on_response_completed
+ assert_response_event(
+ response_event,
+ expected_response={
+ "status": 401,
+ "statusText": "Unauthorized",
+ },
+ )
+
+
+async def test_default(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, url
+):
+ request = await setup_blocked_request("authRequired")
+
+ # Additionally subscribe to all network events
+ await subscribe_events(events=["network"])
+
+ # Track all received network.responseCompleted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ RESPONSE_COMPLETED_EVENT, on_event
+ )
+
+ # continueWithAuth using action "default" should show the authentication
+ # prompt and no new network event should be generated.
+ await bidi_session.network.continue_with_auth(request=request, action="default")
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener()
+
+
+async def test_provideCredentials(
+ setup_blocked_request, subscribe_events, bidi_session, url
+):
+ # Setup unique username / password because browsers cache credentials.
+ username = "test_provideCredentials"
+ password = "test_provideCredentials_password"
+ request = await setup_blocked_request("authRequired", username=username, password=password)
+
+ # Additionally subscribe to network.responseCompleted
+ await subscribe_events(events=[RESPONSE_COMPLETED_EVENT])
+
+ # Track all received network.responseCompleted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ RESPONSE_COMPLETED_EVENT, on_event
+ )
+
+ credentials = AuthCredentials(username=username, password=password)
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials=credentials
+ )
+
+ # TODO: At the moment, the specification does not expect to receive a
+ # responseCompleted event for each authentication attempt, so only assert
+ # the last event. See https://github.com/w3c/webdriver-bidi/issues/627
+
+ # Wait until a a responseCompleted event with status 200 OK is received.
+ wait = AsyncPoll(bidi_session, message="Didn't receive response completed events")
+ await wait.until(lambda _: len(events) > 0 and events[-1]["response"]["status"] == 200)
+
+ remove_listener()
+
+
+async def test_provideCredentials_wrong_credentials(
+ setup_blocked_request, subscribe_events, bidi_session, wait_for_event, url
+):
+ # Setup unique username / password because browsers cache credentials.
+ username = "test_provideCredentials_wrong_credentials"
+ password = "test_provideCredentials_wrong_credentials_password"
+ request = await setup_blocked_request("authRequired", username=username, password=password)
+
+ # Additionally subscribe to network.responseCompleted
+ await subscribe_events(events=[RESPONSE_COMPLETED_EVENT])
+
+ # Track all received network.responseCompleted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ RESPONSE_COMPLETED_EVENT, on_event
+ )
+
+ on_auth_required = wait_for_event(AUTH_REQUIRED_EVENT)
+
+ wrong_credentials = AuthCredentials(username=username, password="wrong_password")
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials=wrong_credentials
+ )
+
+ # We expect to get another authRequired event after providing wrong credentials
+ await on_auth_required
+
+ # Continue with the correct credentials
+ correct_credentials = AuthCredentials(username=username, password=password)
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials=correct_credentials
+ )
+
+ # TODO: At the moment, the specification does not expect to receive a
+ # responseCompleted event for each authentication attempt, so only assert
+ # the last event. See https://github.com/w3c/webdriver-bidi/issues/627
+
+ # Wait until a a responseCompleted event with status 200 OK is received.
+ wait = AsyncPoll(bidi_session, message="Didn't receive response completed events")
+ await wait.until(lambda _: len(events) > 0 and events[-1]["response"]["status"] == 200)
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/invalid.py
new file mode 100644
index 0000000000..dc21d0bc53
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/continue_with_auth/invalid.py
@@ -0,0 +1,181 @@
+import pytest
+import webdriver.bidi.error as error
+
+from .. import PAGE_EMPTY_TEXT, RESPONSE_COMPLETED_EVENT
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", ["beforeRequestSent", "responseStarted"])
+async def test_params_request_invalid_phase(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request(value)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(request=request, action="cancel")
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_request_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(request=value, action="cancel")
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_request_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_with_auth(request=value, action="cancel")
+
+
+async def test_params_request_no_such_request(
+ bidi_session, setup_network_test, wait_for_event, fetch, url
+):
+ await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ await fetch(text_url)
+
+ response_completed_event = await on_response_completed
+ request = response_completed_event["request"]["request"]
+
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_with_auth(request=request, action="cancel")
+
+
+async def test_params_request_no_such_request_after_cancel(
+ setup_blocked_request, bidi_session, subscribe_events, wait_for_event
+):
+ request = await setup_blocked_request("authRequired")
+
+ await subscribe_events(events=[RESPONSE_COMPLETED_EVENT])
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ await bidi_session.network.continue_with_auth(request=request, action="cancel")
+ await on_response_completed
+
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_with_auth(request=request, action="cancel")
+
+
+async def test_params_request_no_such_request_after_provideCredentials(
+ setup_blocked_request, bidi_session, subscribe_events, wait_for_event
+):
+ # Setup unique username / password because browsers cache credentials.
+ username = "test_params_request_no_such_request_after_provideCredentials"
+ password = "test_params_request_no_such_request_after_provideCredentials_password"
+ request = await setup_blocked_request("authRequired", username=username, password=password)
+
+ await subscribe_events(events=[RESPONSE_COMPLETED_EVENT])
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ credentials = {
+ "type": "password",
+ "username": username,
+ "password": password,
+ }
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials=credentials
+ )
+ await on_response_completed
+
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.continue_with_auth(request=request, action="cancel")
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_action_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("authRequired")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(request=request, action=value)
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_action_invalid_value(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("authRequired")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(request=request, action=value)
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ {"type": "password", "password": "foo"},
+ {"type": "password", "username": "foo"},
+ {
+ "type": "password",
+ },
+ {
+ "username": "foo",
+ "password": "bar",
+ },
+ None,
+ ],
+ ids=[
+ "missing username",
+ "missing password",
+ "missing username and password",
+ "missing type",
+ "missing credentials",
+ ],
+)
+async def test_params_action_provideCredentials_invalid_credentials(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("authRequired")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials=value
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_action_provideCredentials_credentials_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("authRequired")
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials={"type": value,}
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_action_provideCredentials_credentials_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("authRequired")
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials={"type": value,}
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_action_provideCredentials_credentials_username_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("authRequired")
+ credentials = {"type": "password", "username": value, "password": "foo"}
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials=credentials
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_action_provideCredentials_credentials_password_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("authRequired")
+ credentials = {"type": "password", "username": "foo", "password": value}
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.continue_with_auth(
+ request=request, action="provideCredentials", credentials=credentials
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/invalid.py
new file mode 100644
index 0000000000..ead87c1a37
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/invalid.py
@@ -0,0 +1,45 @@
+import pytest
+import webdriver.bidi.error as error
+
+from .. import PAGE_EMPTY_TEXT, RESPONSE_COMPLETED_EVENT
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_params_request_invalid_phase(setup_blocked_request, bidi_session):
+ request = await setup_blocked_request("authRequired")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.fail_request(request=request)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_request_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.fail_request(request=value)
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_request_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.fail_request(request=value)
+
+
+async def test_params_request_no_such_request(
+ bidi_session, setup_network_test, wait_for_event, fetch, url
+):
+ await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ await fetch(text_url)
+
+ response_completed_event = await on_response_completed
+ request = response_completed_event["request"]["request"]
+
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.fail_request(request=request)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/request.py b/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/request.py
new file mode 100644
index 0000000000..368e46ebe5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/fail_request/request.py
@@ -0,0 +1,29 @@
+import pytest
+
+from .. import (
+ assert_fetch_error_event,
+ PAGE_EMPTY_TEXT,
+ FETCH_ERROR_EVENT,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("phase", ["beforeRequestSent", "responseStarted"])
+async def test_phases(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, url, phase
+):
+ request = await setup_blocked_request(phase)
+ await subscribe_events(events=[FETCH_ERROR_EVENT])
+
+ on_fetch_error = wait_for_event(FETCH_ERROR_EVENT)
+ await bidi_session.network.fail_request(request=request)
+ await on_fetch_error
+
+ fetch_error_event = await on_fetch_error
+ expected_request = {"method": "GET", "url": url(PAGE_EMPTY_TEXT)}
+ assert_fetch_error_event(
+ fetch_error_event,
+ expected_request=expected_request,
+ redirect_count=0,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/fetch_error.py b/testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/fetch_error.py
new file mode 100644
index 0000000000..025da87c92
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/fetch_error/fetch_error.py
@@ -0,0 +1,297 @@
+import asyncio
+
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from tests.support.sync import AsyncPoll
+
+from .. import (
+ assert_fetch_error_event,
+ assert_response_event,
+ FETCH_ERROR_EVENT,
+ PAGE_EMPTY_HTML,
+ RESPONSE_COMPLETED_EVENT,
+ PAGE_INVALID_URL,
+)
+
+
+@pytest.mark.asyncio
+async def test_subscribe_status(
+ bidi_session,
+ subscribe_events,
+ top_context,
+ wait_for_event,
+ wait_for_future_safe,
+ url,
+ fetch,
+):
+ await subscribe_events(events=[FETCH_ERROR_EVENT])
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url(PAGE_EMPTY_HTML),
+ wait="complete",
+ )
+
+ # Track all received network.beforeRequestSent events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(FETCH_ERROR_EVENT, on_event)
+
+ on_fetch_error = wait_for_event(FETCH_ERROR_EVENT)
+ asyncio.ensure_future(fetch(PAGE_INVALID_URL))
+ await wait_for_future_safe(on_fetch_error)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": PAGE_INVALID_URL}
+ assert_fetch_error_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+
+ await bidi_session.session.unsubscribe(events=[FETCH_ERROR_EVENT])
+
+ # Fetch the invalid url again, with an additional parameter to bypass the
+ # cache and check no new event is received.
+ asyncio.ensure_future(fetch(PAGE_INVALID_URL))
+ await asyncio.sleep(0.5)
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_aborted_request(
+ wait_for_event,
+ wait_for_future_safe,
+ setup_network_test,
+ url,
+ fetch,
+):
+ network_events = await setup_network_test(events=[FETCH_ERROR_EVENT])
+ events = network_events[FETCH_ERROR_EVENT]
+
+ # Prepare a slow url
+ slow_url = url(
+ "/webdriver/tests/bidi/browsing_context/support/empty.txt?pipe=trickle(d10)"
+ )
+ on_fetch_error = wait_for_event(FETCH_ERROR_EVENT)
+ asyncio.ensure_future(fetch(PAGE_INVALID_URL, timeout_in_seconds=0))
+ fetch_error_event = await wait_for_future_safe(on_fetch_error)
+
+
+@pytest.mark.asyncio
+async def test_iframe_load(
+ bidi_session,
+ top_context,
+ setup_network_test,
+ inline,
+):
+ network_events = await setup_network_test(events=[FETCH_ERROR_EVENT])
+ events = network_events[FETCH_ERROR_EVENT]
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=inline(f"<iframe src='{PAGE_INVALID_URL}'></iframe>"),
+ )
+
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 1)
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context = contexts[0]["children"][0]
+
+ assert len(events) == 1
+ assert_fetch_error_event(
+ events[0],
+ expected_request={"url": PAGE_INVALID_URL},
+ context=frame_context["context"],
+ )
+
+
+@pytest.mark.asyncio
+async def test_navigation_id(
+ bidi_session,
+ top_context,
+ wait_for_event,
+ url,
+ fetch,
+ setup_network_test,
+ wait_for_future_safe,
+):
+ await setup_network_test(events=[FETCH_ERROR_EVENT])
+
+ on_fetch_error = wait_for_event(FETCH_ERROR_EVENT)
+ asyncio.ensure_future(fetch(PAGE_INVALID_URL))
+ fetch_error_event = await wait_for_future_safe(on_fetch_error)
+
+ expected_request = {"method": "GET", "url": PAGE_INVALID_URL}
+ assert_fetch_error_event(
+ fetch_error_event,
+ expected_request=expected_request,
+ )
+ # Check that requests not related to a navigation have no navigation id.
+ assert fetch_error_event["navigation"] is None
+
+ on_fetch_error = wait_for_event(FETCH_ERROR_EVENT)
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=PAGE_INVALID_URL,
+ )
+ fetch_error_event = await wait_for_future_safe(on_fetch_error)
+
+ expected_request = {"method": "GET", "url": PAGE_INVALID_URL}
+ assert_fetch_error_event(
+ fetch_error_event,
+ expected_request=expected_request,
+ navigation=result["navigation"],
+ )
+ assert fetch_error_event["navigation"] == result["navigation"]
+
+
+@pytest.mark.parametrize(
+ "method, has_preflight",
+ [
+ ("GET", False),
+ ("HEAD", False),
+ ("POST", False),
+ ("OPTIONS", True),
+ ("DELETE", True),
+ ("PATCH", True),
+ ("PUT", True),
+ ],
+)
+@pytest.mark.asyncio
+async def test_request_method(
+ bidi_session,
+ wait_for_event,
+ wait_for_future_safe,
+ fetch,
+ setup_network_test,
+ method,
+ has_preflight,
+):
+ network_events = await setup_network_test(events=[FETCH_ERROR_EVENT])
+ events = network_events[FETCH_ERROR_EVENT]
+
+ asyncio.ensure_future(fetch(PAGE_INVALID_URL, method=method))
+
+ # Requests which might update the server will also fail the CORS preflight
+ # request which uses the OPTIONS method.
+ expected_events = 2 if has_preflight else 1
+
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= expected_events)
+ assert len(events) == expected_events
+
+ # TODO: At the moment the event order for preflight requests differs between
+ # Chrome and Firefox so we cannot assume the order of fetchError events.
+ # See https://bugzilla.mozilla.org/show_bug.cgi?id=1879402.
+
+ # Check that fetch_error events have the expected methods.
+ assert method in [e["request"]["method"] for e in events]
+ if has_preflight:
+ assert "OPTIONS" in [e["request"]["method"] for e in events]
+
+ for event in events:
+ assert_fetch_error_event(
+ event,
+ expected_request={"url": PAGE_INVALID_URL},
+ )
+
+
+@pytest.mark.asyncio
+async def test_redirect_fetch(
+ bidi_session, wait_for_event, url, fetch, setup_network_test
+):
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={PAGE_INVALID_URL}"
+ )
+
+ await setup_network_test(
+ events=[
+ FETCH_ERROR_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+
+ on_fetch_error = wait_for_event(FETCH_ERROR_EVENT)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ asyncio.ensure_future(fetch(redirect_url))
+
+ # Wait until we receive two events, one for the initial request and one for
+ # the redirection.
+ wait = AsyncPoll(bidi_session, timeout=2)
+ fetch_error_event = await on_fetch_error
+ response_completed_event = await on_response_completed
+
+ expected_request = {"method": "GET", "url": redirect_url}
+ assert_response_event(
+ response_completed_event,
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+ expected_request = {"method": "GET", "url": PAGE_INVALID_URL}
+ assert_fetch_error_event(
+ fetch_error_event, expected_request=expected_request, redirect_count=1
+ )
+
+ # Check that both requests share the same requestId
+ assert (
+ fetch_error_event["request"]["request"]
+ == response_completed_event["request"]["request"]
+ )
+
+
+@pytest.mark.asyncio
+async def test_redirect_navigation(
+ bidi_session, top_context, wait_for_event, url, setup_network_test
+):
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={PAGE_INVALID_URL}"
+ )
+
+ await setup_network_test(
+ events=[
+ FETCH_ERROR_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+
+ on_fetch_error = wait_for_event(FETCH_ERROR_EVENT)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=redirect_url,
+ )
+
+ wait = AsyncPoll(bidi_session, timeout=2)
+ fetch_error_event = await on_fetch_error
+ response_completed_event = await on_response_completed
+
+ expected_request = {"method": "GET", "url": redirect_url}
+ assert_response_event(
+ response_completed_event,
+ expected_request=expected_request,
+ navigation=result["navigation"],
+ redirect_count=0,
+ )
+ expected_request = {"method": "GET", "url": PAGE_INVALID_URL}
+ assert_fetch_error_event(
+ fetch_error_event,
+ expected_request=expected_request,
+ navigation=result["navigation"],
+ redirect_count=1,
+ )
+
+ # Check that all events share the same requestId
+ assert (
+ fetch_error_event["request"]["request"]
+ == response_completed_event["request"]["request"]
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/invalid.py
new file mode 100644
index 0000000000..49177bcb4a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/invalid.py
@@ -0,0 +1,407 @@
+# META: timeout=long
+
+import pytest
+import webdriver.bidi.error as error
+
+from .. import (
+ create_cookie_header,
+ create_header,
+ PAGE_EMPTY_TEXT,
+ RESPONSE_COMPLETED_EVENT,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_body_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request, body=value)
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_body_invalid_value(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request, body=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_body_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request, body={"type": value, "value": "foo"}
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_body_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request, body={"type": value, "value": "foo"}
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_body_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request, body={"type": "string", "value": value}
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", {}])
+async def test_params_cookies_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request, cookies=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_cookies_cookie_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request, cookies=[value])
+
+
+@pytest.mark.parametrize(
+ "value",
+ [{}, {"name": "name"}, {"value": {"type": "string", "value": "foo"}}],
+ ids=[
+ "empty object",
+ "missing value",
+ "missing name",
+ ],
+)
+async def test_params_cookies_cookie_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[value],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_name_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"name": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_cookies_cookie_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_cookies_cookie_value_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_value_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_cookies_cookie_value_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("property", ["domain", "expiry", "path", "sameSite"])
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_cookies_cookie_value_string_properties_invalid_type(
+ setup_blocked_request, bidi_session, property, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={property: value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_cookies_cookie_value_same_site_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"sameSite": value})],
+ )
+
+
+@pytest.mark.parametrize("property", ["httpOnly", "secure"])
+@pytest.mark.parametrize("value", [42, "foo", {}, []])
+async def test_params_cookies_cookie_value_bool_properties_invalid_type(
+ setup_blocked_request, bidi_session, property, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={property: value})],
+ )
+
+
+@pytest.mark.parametrize("value", [False, "foo", {}, []])
+async def test_params_cookies_cookie_value_max_age_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"maxAge": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [4.3])
+async def test_params_cookies_cookie_value_max_age_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(overrides={"maxAge": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_cookies_cookie_value_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ cookies=[create_cookie_header(value_overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", {}])
+async def test_params_headers_invalid_type(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request, headers=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_headers_header_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request, headers=[value])
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_name_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ headers=[create_header(overrides={"name": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_headers_header_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ headers=[create_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [{}, {"type": "string"}, {"value": "foo"}])
+async def test_params_headers_header_value_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ headers=[create_header(overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_value_type_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ headers=[create_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_headers_header_value_type_invalid_value(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ headers=[create_header(value_overrides={"type": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_headers_header_value_value_invalid_type(
+ setup_blocked_request, bidi_session, value
+):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(
+ request=request,
+ headers=[create_header(value_overrides={"value": value})],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_request_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=value)
+
+
+@pytest.mark.parametrize("value", ["", "foo"])
+async def test_params_request_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.provide_response(request=value)
+
+
+async def test_params_request_no_such_request(
+ bidi_session, setup_network_test, wait_for_event, fetch, url
+):
+ await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ await fetch(text_url)
+
+ response_completed_event = await on_response_completed
+ request = response_completed_event["request"]["request"]
+
+ with pytest.raises(error.NoSuchRequestException):
+ await bidi_session.network.provide_response(request=request)
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_reason_phrase_invalid_type(setup_blocked_request,
+ bidi_session,
+ value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request,
+ reason_phrase=value)
+
+
+@pytest.mark.parametrize("value", [False, "foo", {}, []])
+async def test_params_status_code_invalid_type(setup_blocked_request, bidi_session,
+ value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request,
+ status_code=value)
+
+
+@pytest.mark.parametrize("value", [-1, 4.3])
+async def test_params_status_code_invalid_value(setup_blocked_request, bidi_session, value):
+ request = await setup_blocked_request("beforeRequestSent")
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.provide_response(request=request,
+ status_code=value)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/request.py b/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/request.py
new file mode 100644
index 0000000000..de9492f0a5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/provide_response/request.py
@@ -0,0 +1,67 @@
+import pytest
+
+from webdriver.bidi.modules.network import AuthCredentials
+
+from tests.support.sync import AsyncPoll
+
+from .. import AUTH_REQUIRED_EVENT, RESPONSE_COMPLETED_EVENT, RESPONSE_STARTED_EVENT
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("navigate", [False, True], ids=["fetch", "navigate"])
+async def test_provide_response_auth_required(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, navigate
+):
+ request = await setup_blocked_request("authRequired", navigate=navigate)
+
+ await subscribe_events(
+ events=[
+ AUTH_REQUIRED_EVENT,
+ "browsingContext.load",
+ ]
+ )
+
+ # For requests blocked on authRequired, providing a response with no
+ # additional argument should just lead to another authRequired event.
+ on_auth_required = wait_for_event(AUTH_REQUIRED_EVENT)
+
+ await bidi_session.network.provide_response(request=request)
+
+ await on_auth_required
+
+
+@pytest.mark.parametrize("phase", ["beforeRequestSent", "responseStarted"])
+@pytest.mark.parametrize("navigate", [False, True], ids=["fetch", "navigate"])
+async def test_provide_response_phase(
+ setup_blocked_request, subscribe_events, wait_for_event, bidi_session, phase, navigate
+):
+ request = await setup_blocked_request(phase, navigate=navigate)
+
+ await subscribe_events(
+ events=[
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ "browsingContext.load",
+ ]
+ )
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ if phase == "beforeRequestSent":
+ # For a request blocked on beforeRequestSent, a responseStarted event is
+ # also expected.
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+
+ if navigate:
+ on_load = wait_for_event("browsingContext.load")
+
+ await bidi_session.network.provide_response(request=request)
+
+ await on_response_completed
+
+ if phase == "beforeRequestSent":
+ await on_response_started
+
+ if navigate:
+ await on_load
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/invalid.py
new file mode 100644
index 0000000000..4b3526bfc3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/invalid.py
@@ -0,0 +1,28 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_intercept_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.network.remove_intercept(intercept=value)
+
+
+@pytest.mark.parametrize("value", ["foo"])
+async def test_params_intercept_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchInterceptException):
+ await bidi_session.network.remove_intercept(intercept=value)
+
+
+async def test_params_intercept_removed_intercept(bidi_session, add_intercept):
+ intercept = await add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[{"type": "string", "pattern": "https://example.com"}],
+ )
+
+ await bidi_session.network.remove_intercept(intercept=intercept)
+
+ with pytest.raises(error.NoSuchInterceptException):
+ await bidi_session.network.remove_intercept(intercept=intercept)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/remove_intercept.py b/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/remove_intercept.py
new file mode 100644
index 0000000000..7935b94d2d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/remove_intercept/remove_intercept.py
@@ -0,0 +1,106 @@
+# META: timeout=long
+
+import asyncio
+import pytest
+
+from .. import (
+ assert_before_request_sent_event,
+ assert_response_event,
+ PAGE_EMPTY_HTML,
+ PAGE_EMPTY_TEXT,
+ PAGE_OTHER_TEXT,
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ RESPONSE_STARTED_EVENT,
+)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("phase", [
+ "beforeRequestSent",
+ "responseStarted",
+])
+async def test_remove_intercept(
+ bidi_session, wait_for_event, url, setup_network_test, add_intercept, top_context, wait_for_future_safe, phase
+):
+ network_events = await setup_network_test(
+ events=[
+ BEFORE_REQUEST_SENT_EVENT,
+ RESPONSE_STARTED_EVENT,
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ before_request_sent_events = network_events[BEFORE_REQUEST_SENT_EVENT]
+ response_started_events = network_events[RESPONSE_STARTED_EVENT]
+ response_completed_events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ intercept = await add_intercept(
+ phases=[phase],
+ url_patterns=[{"type": "string", "pattern": text_url}],
+ )
+
+ on_network_event = wait_for_event(f"network.{phase}")
+
+ # Request to top_context should be blocked and run into a timeout.
+ # TODO(https://github.com/w3c/webdriver-bidi/issues/188): Use a timeout argument when available.
+ with pytest.raises(asyncio.TimeoutError):
+ await asyncio.wait_for(
+ asyncio.shield(bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=text_url, wait="complete")),
+ timeout=2.0,
+ )
+
+ await wait_for_future_safe(on_network_event)
+
+ assert len(before_request_sent_events) == 1
+
+ if phase == "beforeRequestSent":
+ assert len(response_started_events) == 0
+ assert_before_request_sent_event(
+ before_request_sent_events[0], is_blocked=True, intercepts=[intercept]
+ )
+ elif phase == "responseStarted":
+ assert len(response_started_events) == 1
+ assert_before_request_sent_event(
+ before_request_sent_events[0], is_blocked=False
+ )
+ assert_response_event(
+ response_started_events[0], is_blocked=True, intercepts=[intercept]
+ )
+
+ # Check that we did not receive response completed events.
+ assert len(response_completed_events) == 0
+
+ # Remove the intercept
+ await bidi_session.network.remove_intercept(intercept=intercept)
+
+ # The next request should not be blocked
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await bidi_session.browsing_context.navigate(context=top_context["context"], url=text_url, wait="complete")
+ await wait_for_future_safe(on_response_completed)
+
+ # Assert the network events have the expected interception properties
+ assert len(before_request_sent_events) == 2
+ assert_before_request_sent_event(before_request_sent_events[1], is_blocked=False)
+
+ if phase == "beforeRequestSent":
+ assert len(response_started_events) == 1
+ assert_response_event(response_started_events[0], is_blocked=False)
+ elif phase == "responseStarted":
+ assert len(response_started_events) == 2
+ assert_response_event(response_started_events[1], is_blocked=False)
+
+ assert len(response_completed_events) == 1
+ assert_response_event(response_completed_events[0], is_blocked=False)
+
+
+@pytest.mark.asyncio
+async def test_return_value(bidi_session, add_intercept):
+ intercept = await add_intercept(
+ phases=["beforeRequestSent"],
+ url_patterns=[],
+ )
+
+ result = await bidi_session.network.remove_intercept(intercept=intercept)
+ assert result == {}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py
new file mode 100644
index 0000000000..b9b4ae727e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed.py
@@ -0,0 +1,370 @@
+import asyncio
+from urllib.parse import quote
+
+import pytest
+
+from tests.support.sync import AsyncPoll
+
+from .. import (
+ assert_response_event,
+ HTTP_STATUS_AND_STATUS_TEXT,
+ PAGE_EMPTY_HTML,
+ PAGE_EMPTY_IMAGE,
+ PAGE_EMPTY_SCRIPT,
+ PAGE_EMPTY_SVG,
+ PAGE_EMPTY_TEXT,
+ RESPONSE_COMPLETED_EVENT,
+)
+
+
+@pytest.mark.asyncio
+async def test_subscribe_status(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe, url, fetch):
+ await subscribe_events(events=[RESPONSE_COMPLETED_EVENT])
+
+ # Track all received network.responseCompleted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(
+ RESPONSE_COMPLETED_EVENT, on_event
+ )
+
+ html_url = url(PAGE_EMPTY_HTML)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=html_url,
+ wait="complete",
+ )
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": html_url}
+ expected_response = {
+ "url": url(PAGE_EMPTY_HTML),
+ "fromCache": False,
+ "mimeType": "text/html",
+ "status": 200,
+ "statusText": "OK",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(text_url)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 2
+ expected_request = {"method": "GET", "url": text_url}
+ expected_response = {
+ "url": text_url,
+ "fromCache": False,
+ "mimeType": "text/plain",
+ "status": 200,
+ "statusText": "OK",
+ }
+ assert_response_event(
+ events[1],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+ await bidi_session.session.unsubscribe(events=[RESPONSE_COMPLETED_EVENT])
+
+ # Fetch the text url again, with an additional parameter to bypass the cache
+ # and check no new event is received.
+ await fetch(f"{text_url}?nocache")
+ await asyncio.sleep(0.5)
+ assert len(events) == 2
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_iframe_load(
+ bidi_session,
+ top_context,
+ setup_network_test,
+ test_page,
+ test_page_same_origin_frame,
+):
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=test_page_same_origin_frame,
+ wait="complete",
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context = contexts[0]["children"][0]
+
+ assert len(events) == 2
+ assert_response_event(
+ events[0],
+ expected_request={"url": test_page_same_origin_frame},
+ context=top_context["context"],
+ )
+ assert_response_event(
+ events[1],
+ expected_request={"url": test_page},
+ context=frame_context["context"],
+ )
+
+
+@pytest.mark.asyncio
+async def test_load_page_twice(
+ bidi_session, top_context, wait_for_event, wait_for_future_safe, url, setup_network_test
+):
+ html_url = url(PAGE_EMPTY_HTML)
+
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=html_url,
+ wait="complete",
+ )
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": html_url}
+ expected_response = {
+ "url": html_url,
+ "fromCache": False,
+ "mimeType": "text/html",
+ "status": 200,
+ "statusText": "OK",
+ "protocol": "http/1.1",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ navigation=result["navigation"],
+ redirect_count=0,
+ )
+
+
+@pytest.mark.parametrize(
+ "status, status_text",
+ [(status, text) for (status, text) in HTTP_STATUS_AND_STATUS_TEXT if status not in [101, 407]],
+)
+@pytest.mark.asyncio
+async def test_response_status(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test, status, status_text
+):
+ status_url = url(
+ f"/webdriver/tests/support/http_handlers/status.py?status={status}&nocache={RESPONSE_COMPLETED_EVENT}"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(status_url)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": status_url}
+ expected_response = {
+ "url": status_url,
+ "fromCache": False,
+ "mimeType": "text/plain",
+ "status": status,
+ "statusText": status_text,
+ "protocol": "http/1.1",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_response_headers(wait_for_event, wait_for_future_safe, url, fetch, setup_network_test):
+ headers_url = url(
+ "/webdriver/tests/support/http_handlers/headers.py?header=foo:bar&header=baz:biz"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(headers_url, method="GET")
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+
+ expected_request = {"method": "GET", "url": headers_url}
+ expected_response = {
+ "url": headers_url,
+ "fromCache": False,
+ "mimeType": "text/plain",
+ "status": 200,
+ "statusText": "OK",
+ "headers": (
+ {"name": "foo", "value": {"type": "string", "value": "bar"}},
+ {"name": "baz", "value": {"type": "string", "value": "biz"}},
+ ),
+ "protocol": "http/1.1",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.parametrize(
+ "page_url, mime_type",
+ [
+ (PAGE_EMPTY_HTML, "text/html"),
+ (PAGE_EMPTY_TEXT, "text/plain"),
+ (PAGE_EMPTY_SCRIPT, "text/javascript"),
+ (PAGE_EMPTY_IMAGE, "image/png"),
+ (PAGE_EMPTY_SVG, "image/svg+xml"),
+ ],
+)
+@pytest.mark.asyncio
+async def test_response_mime_type_file(
+ url, wait_for_event, wait_for_future_safe, fetch, setup_network_test, page_url, mime_type
+):
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(url(page_url), method="GET")
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+
+ expected_request = {"method": "GET", "url": url(page_url)}
+ expected_response = {"url": url(page_url), "mimeType": mime_type}
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_redirect(bidi_session, url, fetch, setup_network_test):
+ text_url = url(PAGE_EMPTY_TEXT)
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={text_url}"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ await fetch(redirect_url, method="GET")
+
+ # Wait until we receive two events, one for the initial request and one for
+ # the redirection.
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+
+ assert len(events) == 2
+ expected_request = {"method": "GET", "url": redirect_url}
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+ expected_request = {"method": "GET", "url": text_url}
+ assert_response_event(
+ events[1], expected_request=expected_request, redirect_count=1
+ )
+
+ # Check that both requests share the same requestId
+ assert events[0]["request"]["request"] == events[1]["request"]["request"]
+
+
+@pytest.mark.parametrize(
+ "protocol,parameters",
+ [
+ ("http", ""),
+ ("https", ""),
+ ("https", {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"}),
+ ],
+ ids=["http", "https", "https coop"],
+)
+@pytest.mark.asyncio
+async def test_redirect_document(
+ bidi_session, new_tab, url, setup_network_test, inline, protocol, parameters
+):
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ # The test starts on a url on the alternate domain, potentially with https
+ # and coop headers.
+ initial_url = inline(
+ "<div>bar</div>",
+ domain="alt",
+ protocol=protocol,
+ parameters=parameters,
+ )
+ first_navigate = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=initial_url,
+ wait="complete",
+ )
+
+ # Then navigate to a cross domain page, which will redirect back to the
+ # initial url.
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={quote(initial_url)}"
+ )
+ second_navigate = await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=redirect_url,
+ wait="complete",
+ )
+
+ # Wait until we receive three events:
+ # - one for the initial request
+ # - two for the second navigation and its redirect
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 3)
+ assert len(events) == 3
+
+ expected_request = {"method": "GET", "url": initial_url}
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ navigation=first_navigate["navigation"],
+ )
+ expected_request = {"method": "GET", "url": redirect_url}
+ assert_response_event(
+ events[1],
+ expected_request=expected_request,
+ redirect_count=0,
+ navigation=second_navigate["navigation"],
+ )
+ expected_request = {"method": "GET", "url": initial_url}
+ assert_response_event(
+ events[2],
+ expected_request=expected_request,
+ redirect_count=1,
+ navigation=second_navigate["navigation"],
+ )
+
+ # Check that the last 2 requests share the same request id
+ assert events[1]["request"]["request"] == events[2]["request"]["request"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_cached.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_cached.py
new file mode 100644
index 0000000000..6457e7d412
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_cached.py
@@ -0,0 +1,191 @@
+import pytest
+import random
+
+from tests.support.sync import AsyncPoll
+
+from .. import assert_response_event, PAGE_EMPTY_TEXT, RESPONSE_COMPLETED_EVENT
+
+
+@pytest.mark.asyncio
+async def test_cached(
+ wait_for_event,
+ wait_for_future_safe,
+ url,
+ fetch,
+ setup_network_test,
+):
+ network_events = await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ cached_url = url(
+ f"/webdriver/tests/support/http_handlers/cached.py?status=200&nocache={random.random()}"
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(cached_url)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": cached_url}
+
+ # The first request/response is used to fill the browser cache, so we expect
+ # fromCache to be False here.
+ expected_response = {
+ "url": cached_url,
+ "fromCache": False,
+ "status": 200,
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(cached_url)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 2
+
+ # The second request for the same URL has to be read from the local cache.
+ expected_response = {
+ "url": cached_url,
+ "fromCache": True,
+ "status": 200,
+ }
+ assert_response_event(
+ events[1],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+
+@pytest.mark.asyncio
+async def test_cached_redirect(
+ bidi_session,
+ url,
+ fetch,
+ setup_network_test,
+):
+ network_events = await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ cached_url = url(
+ f"/webdriver/tests/support/http_handlers/cached.py?status=301&location={text_url}&nocache={random.random()}"
+ )
+
+ await fetch(cached_url)
+
+ # Expect two events, one for the initial request and one for the redirect.
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+ assert len(events) == 2
+
+ # The first request/response is used to fill the cache, so we expect
+ # fromCache to be False here.
+ expected_request = {"method": "GET", "url": cached_url}
+ expected_response = {
+ "url": cached_url,
+ "fromCache": False,
+ "status": 301,
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ # The second request is the redirect
+ redirected_request = {"method": "GET", "url": text_url}
+ redirected_response = {"url": text_url, "status": 200}
+ assert_response_event(
+ events[1],
+ expected_request=redirected_request,
+ expected_response=redirected_response,
+ )
+
+ await fetch(cached_url)
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 4)
+ assert len(events) == 4
+
+ # The third request hits cached_url again and has to be read from the local cache.
+ expected_response = {
+ "url": cached_url,
+ "fromCache": True,
+ "status": 301,
+ }
+ assert_response_event(
+ events[2],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ # The fourth request is the redirect
+ assert_response_event(
+ events[3],
+ expected_request=redirected_request,
+ expected_response=redirected_response,
+ )
+
+
+@pytest.mark.asyncio
+async def test_cached_revalidate(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test
+):
+ network_events = await setup_network_test(
+ events=[
+ RESPONSE_COMPLETED_EVENT,
+ ]
+ )
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ revalidate_url = url(
+ f"/webdriver/tests/support/http_handlers/must-revalidate.py?nocache={random.random()}"
+ )
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(revalidate_url)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": revalidate_url}
+ expected_response = {
+ "url": revalidate_url,
+ "fromCache": False,
+ "status": 200,
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+
+ # Note that we pass a specific header so that the must-revalidate.py handler
+ # can decide to return a 304 without having to use another URL.
+ await fetch(revalidate_url, headers={"return-304": "true"})
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 2
+
+ # Here fromCache should still be false, because for a 304 response the response
+ # cache state is "validated" and fromCache is only true if cache state is "local"
+ expected_response = {
+ "url": revalidate_url,
+ "fromCache": False,
+ "status": 304,
+ }
+ assert_response_event(
+ events[1],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py
new file mode 100644
index 0000000000..36e3da667e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py
@@ -0,0 +1,55 @@
+# TODO(#42482): Merge this file with response_completed.py
+#
+# The status codes in this file are currently problematic in some implementations.
+#
+# The only mechanism currently provided by WPT to disable subtests with
+# expectations is to disable the entire file. As such, this file is a copy of
+# response_completed.py with the problematic status codes extracted.
+#
+# Once it is possible to disable subtests, this file should be merged with
+# response_completed.py.
+
+import pytest
+
+from .. import (
+ assert_response_event,
+ HTTP_STATUS_AND_STATUS_TEXT,
+ RESPONSE_COMPLETED_EVENT,
+)
+
+
+@pytest.mark.parametrize(
+ "status, status_text",
+ [(status, text) for (status, text) in HTTP_STATUS_AND_STATUS_TEXT if status in [101, 407]],
+)
+@pytest.mark.asyncio
+async def test_response_status(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test, status, status_text
+):
+ status_url = url(
+ f"/webdriver/tests/support/http_handlers/status.py?status={status}&nocache={RESPONSE_COMPLETED_EVENT}"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT])
+ events = network_events[RESPONSE_COMPLETED_EVENT]
+
+ on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT)
+ await fetch(status_url)
+ await wait_for_future_safe(on_response_completed)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": status_url}
+ expected_response = {
+ "url": status_url,
+ "fromCache": False,
+ "mimeType": "text/plain",
+ "status": status,
+ "statusText": status_text,
+ "protocol": "http/1.1",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py
new file mode 100644
index 0000000000..dec743e175
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started.py
@@ -0,0 +1,311 @@
+import asyncio
+
+import pytest
+
+from tests.support.sync import AsyncPoll
+
+from .. import (
+ assert_response_event,
+ HTTP_STATUS_AND_STATUS_TEXT,
+ PAGE_EMPTY_HTML,
+ PAGE_EMPTY_IMAGE,
+ PAGE_EMPTY_SCRIPT,
+ PAGE_EMPTY_SVG,
+ PAGE_EMPTY_TEXT,
+ RESPONSE_STARTED_EVENT,
+)
+
+
+@pytest.mark.asyncio
+async def test_subscribe_status(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe, url, fetch):
+ await subscribe_events(events=[RESPONSE_STARTED_EVENT])
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url(PAGE_EMPTY_HTML),
+ wait="complete",
+ )
+
+ # Track all received network.responseStarted events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(RESPONSE_STARTED_EVENT, on_event)
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(text_url)
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": text_url}
+ expected_response = {
+ "url": text_url,
+ "fromCache": False,
+ "mimeType": "text/plain",
+ "status": 200,
+ "statusText": "OK",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+ await bidi_session.session.unsubscribe(events=[RESPONSE_STARTED_EVENT])
+
+ # Fetch the text url again, with an additional parameter to bypass the cache
+ # and check no new event is received.
+ await fetch(f"{text_url}?nocache")
+ await asyncio.sleep(0.5)
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_iframe_load(
+ bidi_session,
+ top_context,
+ setup_network_test,
+ test_page,
+ test_page_same_origin_frame,
+):
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=test_page_same_origin_frame,
+ wait="complete",
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context = contexts[0]["children"][0]
+
+ assert len(events) == 2
+ assert_response_event(
+ events[0],
+ expected_request={"url": test_page_same_origin_frame},
+ context=top_context["context"],
+ )
+ assert_response_event(
+ events[1],
+ expected_request={"url": test_page},
+ context=frame_context["context"],
+ )
+
+
+@pytest.mark.asyncio
+async def test_load_page_twice(
+ bidi_session, top_context, wait_for_event, wait_for_future_safe, url, setup_network_test
+):
+ html_url = url(PAGE_EMPTY_HTML)
+
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ result = await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=html_url,
+ wait="complete",
+ )
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": html_url}
+ expected_response = {
+ "url": html_url,
+ "fromCache": False,
+ "mimeType": "text/html",
+ "status": 200,
+ "statusText": "OK",
+ "protocol": "http/1.1",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ navigation=result["navigation"],
+ redirect_count=0,
+ )
+
+
+@pytest.mark.parametrize(
+ "status, status_text",
+ HTTP_STATUS_AND_STATUS_TEXT,
+)
+@pytest.mark.asyncio
+async def test_response_status(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test, status, status_text
+):
+ status_url = url(
+ f"/webdriver/tests/support/http_handlers/status.py?status={status}&nocache={RESPONSE_STARTED_EVENT}"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(status_url)
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": status_url}
+ expected_response = {
+ "url": status_url,
+ "fromCache": False,
+ "mimeType": "text/plain",
+ "status": status,
+ "statusText": status_text,
+ "protocol": "http/1.1",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_response_headers(wait_for_event, wait_for_future_safe, url, fetch, setup_network_test):
+ headers_url = url(
+ "/webdriver/tests/support/http_handlers/headers.py?header=foo:bar&header=baz:biz"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(headers_url, method="GET")
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+
+ expected_request = {"method": "GET", "url": headers_url}
+ expected_response = {
+ "url": headers_url,
+ "fromCache": False,
+ "mimeType": "text/plain",
+ "status": 200,
+ "statusText": "OK",
+ "headers": (
+ {"name": "foo", "value": {"type": "string", "value": "bar"}},
+ {"name": "baz", "value": {"type": "string", "value": "biz"}},
+ ),
+ "protocol": "http/1.1",
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.parametrize(
+ "page_url, mime_type",
+ [
+ (PAGE_EMPTY_HTML, "text/html"),
+ (PAGE_EMPTY_TEXT, "text/plain"),
+ (PAGE_EMPTY_SCRIPT, "text/javascript"),
+ (PAGE_EMPTY_IMAGE, "image/png"),
+ (PAGE_EMPTY_SVG, "image/svg+xml"),
+ ],
+)
+@pytest.mark.asyncio
+async def test_response_mime_type_file(
+ url, wait_for_event, wait_for_future_safe, fetch, setup_network_test, page_url, mime_type
+):
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(url(page_url), method="GET")
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+
+ expected_request = {"method": "GET", "url": url(page_url)}
+ expected_response = {"url": url(page_url), "mimeType": mime_type}
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_www_authenticate(
+ bidi_session, url, fetch, new_tab, wait_for_event, wait_for_future_safe, setup_network_test
+):
+ auth_url = url(
+ "/webdriver/tests/support/http_handlers/authentication.py?realm=testrealm"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=auth_url,
+ wait="none",
+ )
+
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+
+ expected_request = {"method": "GET", "url": auth_url}
+ expected_response = {
+ "url": auth_url,
+ "authChallenges": [
+ ({"scheme": "Basic", "realm": "testrealm"}),
+ ],
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ redirect_count=0,
+ )
+
+
+@pytest.mark.asyncio
+async def test_redirect(bidi_session, url, fetch, setup_network_test):
+ text_url = url(PAGE_EMPTY_TEXT)
+ redirect_url = url(
+ f"/webdriver/tests/support/http_handlers/redirect.py?location={text_url}"
+ )
+
+ network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT])
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ await fetch(redirect_url, method="GET")
+
+ # Wait until we receive two events, one for the initial request and one for
+ # the redirection.
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+
+ assert len(events) == 2
+ expected_request = {"method": "GET", "url": redirect_url}
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ redirect_count=0,
+ )
+ expected_request = {"method": "GET", "url": text_url}
+ assert_response_event(
+ events[1], expected_request=expected_request, redirect_count=1
+ )
+
+ # Check that both requests share the same requestId
+ assert events[0]["request"]["request"] == events[1]["request"]["request"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started_cached.py b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started_cached.py
new file mode 100644
index 0000000000..2776950b0e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/response_started/response_started_cached.py
@@ -0,0 +1,199 @@
+import pytest
+import random
+
+from tests.support.sync import AsyncPoll
+
+from .. import assert_response_event, PAGE_EMPTY_TEXT, RESPONSE_STARTED_EVENT
+
+
+@pytest.mark.asyncio
+async def test_cached(
+ wait_for_event,
+ wait_for_future_safe,
+ url,
+ fetch,
+ setup_network_test,
+):
+ network_events = await setup_network_test(
+ events=[
+ RESPONSE_STARTED_EVENT,
+ ]
+ )
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ cached_url = url(
+ f"/webdriver/tests/support/http_handlers/cached.py?status=200&nocache={random.random()}"
+ )
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(cached_url)
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+ expected_request = {"method": "GET", "url": cached_url}
+
+ # The first request/response is used to fill the browser cache, so we expect
+ # fromCache to be False here.
+ expected_response = {
+ "url": cached_url,
+ "fromCache": False,
+ "status": 200,
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(cached_url)
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 2
+
+ # The second request for the same URL has to be read from the local cache.
+ expected_response = {
+ "url": cached_url,
+ "fromCache": True,
+ "status": 200,
+ }
+ assert_response_event(
+ events[1],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+
+@pytest.mark.asyncio
+async def test_cached_redirect(
+ bidi_session,
+ url,
+ fetch,
+ setup_network_test,
+):
+ network_events = await setup_network_test(
+ events=[
+ RESPONSE_STARTED_EVENT,
+ ]
+ )
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ text_url = url(PAGE_EMPTY_TEXT)
+ cached_url = url(
+ f"/webdriver/tests/support/http_handlers/cached.py?status=301&location={text_url}&nocache={random.random()}"
+ )
+
+ await fetch(cached_url)
+
+ # Expect two events, one for the initial request and one for the redirect.
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 2)
+ assert len(events) == 2
+
+ # The first request/response is used to fill the cache, so we expect
+ # fromCache to be False here.
+ expected_request = {"method": "GET", "url": cached_url}
+ expected_response = {
+ "url": cached_url,
+ "fromCache": False,
+ "status": 301,
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ # The second request is the redirect
+ redirected_request = {"method": "GET", "url": text_url}
+ redirected_response = {"url": text_url, "status": 200}
+ assert_response_event(
+ events[1],
+ expected_request=redirected_request,
+ expected_response=redirected_response,
+ )
+
+ await fetch(cached_url)
+ wait = AsyncPoll(bidi_session, timeout=2)
+ await wait.until(lambda _: len(events) >= 4)
+ assert len(events) == 4
+
+ # The third request hits cached_url again and has to be read from the local cache.
+ expected_response = {
+ "url": cached_url,
+ "fromCache": True,
+ "status": 301,
+ }
+ assert_response_event(
+ events[2],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ # The fourth request is the redirect
+ assert_response_event(
+ events[3],
+ expected_request=redirected_request,
+ expected_response=redirected_response,
+ )
+
+
+@pytest.mark.parametrize(
+ "method",
+ [
+ "GET",
+ "HEAD",
+ "OPTIONS",
+ ],
+)
+@pytest.mark.asyncio
+async def test_cached_revalidate(
+ wait_for_event, wait_for_future_safe, url, fetch, setup_network_test, method
+):
+ network_events = await setup_network_test(
+ events=[
+ RESPONSE_STARTED_EVENT,
+ ]
+ )
+ events = network_events[RESPONSE_STARTED_EVENT]
+
+ revalidate_url = url(
+ f"/webdriver/tests/support/http_handlers/must-revalidate.py?nocache={random.random()}"
+ )
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+ await fetch(revalidate_url, method=method)
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 1
+ expected_request = {"method": method, "url": revalidate_url}
+ expected_response = {
+ "url": revalidate_url,
+ "fromCache": False,
+ "status": 200,
+ }
+ assert_response_event(
+ events[0],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
+
+ on_response_started = wait_for_event(RESPONSE_STARTED_EVENT)
+
+ # Note that we pass a specific header so that the must-revalidate.py handler
+ # can decide to return a 304 without having to use another URL.
+ await fetch(revalidate_url, method=method, headers={"return-304": "true"})
+ await wait_for_future_safe(on_response_started)
+
+ assert len(events) == 2
+
+ # Here fromCache should still be false, because for a 304 response the response
+ # cache state is "validated" and fromCache is only true if cache state is "local"
+ expected_response = {
+ "url": revalidate_url,
+ "fromCache": False,
+ "status": 304,
+ }
+ assert_response_event(
+ events[1],
+ expected_request=expected_request,
+ expected_response=expected_response,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.html b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.html
new file mode 100644
index 0000000000..69e9da4114
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<html></html>
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.js b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.js
new file mode 100644
index 0000000000..3918c74e44
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.js
@@ -0,0 +1 @@
+"use strict";
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.png b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.png
new file mode 100644
index 0000000000..afb763ce9d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.png
Binary files differ
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.svg b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.svg
new file mode 100644
index 0000000000..158b3aac16
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"></svg>
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.txt b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.txt
new file mode 100644
index 0000000000..c6cac69265
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/empty.txt
@@ -0,0 +1 @@
+empty
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/other.txt b/testing/web-platform/tests/webdriver/tests/bidi/network/support/other.txt
new file mode 100644
index 0000000000..e45c9c2666
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/other.txt
@@ -0,0 +1 @@
+other
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/redirect_http_equiv.html b/testing/web-platform/tests/webdriver/tests/bidi/network/support/redirect_http_equiv.html
new file mode 100644
index 0000000000..9b588c67ef
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/redirect_http_equiv.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<head>
+ <meta http-equiv="refresh" content="0;redirected.html" />
+</head>
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/support/redirected.html b/testing/web-platform/tests/webdriver/tests/bidi/network/support/redirected.html
new file mode 100644
index 0000000000..3732b218cf
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/network/support/redirected.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<html>redirected</html>
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/__init__.py
new file mode 100644
index 0000000000..7feae91f27
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/__init__.py
@@ -0,0 +1,226 @@
+from __future__ import annotations
+from typing import Any, Callable, Mapping
+from webdriver.bidi.modules.script import ContextTarget
+
+from .. import any_int, any_string, recursive_compare
+
+
+def specific_error_response(expected_error: Mapping[str, Any]) -> Callable[[Any], None]:
+ return lambda actual: recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": expected_error,
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ actual)
+
+
+def any_stack_trace(actual: Any) -> None:
+ assert type(actual) is dict
+ assert "callFrames" in actual
+ assert type(actual["callFrames"]) is list
+ for actual_frame in actual["callFrames"]:
+ any_stack_frame(actual_frame)
+
+
+def any_stack_frame(actual: Any) -> None:
+ assert type(actual) is dict
+
+ assert "columnNumber" in actual
+ any_int(actual["columnNumber"])
+
+ assert "functionName" in actual
+ any_string(actual["functionName"])
+
+ assert "lineNumber" in actual
+ any_int(actual["lineNumber"])
+
+ assert "url" in actual
+ any_string(actual["url"])
+
+
+"""Format: List[(expression, expected)]"""
+PRIMITIVE_VALUES: list[tuple[str, dict]] = [
+ ("undefined", {"type": "undefined"}),
+ ("null", {"type": "null"}),
+ ("'foobar'", {"type": "string", "value": "foobar"}),
+ ("'2'", {"type": "string", "value": "2"}),
+ ("NaN", {"type": "number", "value": "NaN"}),
+ ("-0", {"type": "number", "value": "-0"}),
+ ("Infinity", {"type": "number", "value": "Infinity"}),
+ ("-Infinity", {"type": "number", "value": "-Infinity"}),
+ ("3", {"type": "number", "value": 3}),
+ ("1.4", {"type": "number", "value": 1.4}),
+ ("true", {"type": "boolean", "value": True}),
+ ("false", {"type": "boolean", "value": False}),
+ ("42n", {"type": "bigint", "value": "42"}),
+]
+
+
+"""Format: List[(expression, expected)]"""
+REMOTE_VALUES: list[tuple[str, dict]] = [
+ ("(Symbol('foo'))", {"type": "symbol", },),
+ (
+ "[1, 'foo', true, new RegExp(/foo/g), [1]]",
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "string", "value": "foo"},
+ {"type": "boolean", "value": True},
+ {
+ "type": "regexp",
+ "value": {
+ "pattern": "foo",
+ "flags": "g",
+ },
+ },
+ {"type": "array"},
+ ],
+ },
+ ),
+ (
+ "({'foo': {'bar': 'baz'}, 'qux': 'quux', 1: 'fred', '2': 'thud'})",
+ {
+ "type": "object",
+ "value": [
+ ["1", {"type": "string", "value": "fred"}],
+ ["2", {"type": "string", "value": "thud"}],
+ ["foo", {"type": "object"}],
+ ["qux", {"type": "string", "value": "quux"}],
+ ],
+ },
+ ),
+ ("(()=>{})", {"type": "function", },),
+ ("(function(){})", {"type": "function", },),
+ ("(async ()=>{})", {"type": "function", },),
+ ("(async function(){})", {"type": "function", },),
+ ("(function*() { yield 'a'; })", {
+ "type": "function",
+ }),
+ (
+ "new RegExp(/foo/g)",
+ {
+ "type": "regexp",
+ "value": {
+ "pattern": "foo",
+ "flags": "g",
+ },
+ },
+ ),
+ (
+ "new Date(1654004849000)",
+ {
+ "type": "date",
+ "value": "2022-05-31T13:47:29.000Z",
+ },
+ ),
+ (
+ "new Map([[1, 2], ['foo', 'bar'], [true, false], ['baz', [1]]])",
+ {
+ "type": "map",
+ "value": [
+ [
+ {"type": "number", "value": 1},
+ {"type": "number", "value": 2},
+ ],
+ ["foo", {"type": "string", "value": "bar"}],
+ [
+ {"type": "boolean", "value": True},
+ {"type": "boolean", "value": False},
+ ],
+ ["baz", {"type": "array"}],
+ ],
+ },
+ ),
+ (
+ "new Set([1, 'foo', true, [1], new Map([[1,2]])])",
+ {
+ "type": "set",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "string", "value": "foo"},
+ {"type": "boolean", "value": True},
+ {"type": "array"},
+ {"type": "map"},
+ ],
+ },
+ ),
+ ("new WeakMap()", {"type": "weakmap", },),
+ ("new WeakSet()", {"type": "weakset", },),
+ ("new Error('SOME_ERROR_TEXT')", {"type": "error"},),
+ ("[1, 2][Symbol.iterator]()", {
+ "type": "iterator",
+ }),
+ ("'mystring'[Symbol.iterator]()", {
+ "type": "iterator",
+ }),
+ ("(new Set([1,2]))[Symbol.iterator]()", {
+ "type": "iterator",
+ }),
+ ("(new Map([[1,2]]))[Symbol.iterator]()", {
+ "type": "iterator",
+ }),
+ ("new Proxy({}, {})", {
+ "type": "proxy",
+ }),
+ ("(function*() { yield 'a'; })()", {
+ "type": "generator",
+ }),
+ ("(async function*() { yield await Promise.resolve(1); })()", {
+ "type": "generator",
+ }),
+ ("Promise.resolve()", {"type": "promise", },),
+ ("new Int32Array()", {"type": "typedarray", },),
+ ("new ArrayBuffer()", {"type": "arraybuffer", },),
+ (
+ "document.createElement('div')",
+ {
+ "sharedId": any_string,
+ "type": "node",
+ 'value': {
+ 'attributes': {},
+ 'childNodeCount': 0,
+ 'localName': 'div',
+ 'namespaceURI': 'http://www.w3.org/1999/xhtml',
+ 'nodeType': 1,
+ 'shadowRoot': None,
+ }
+ },
+ ),
+ (
+ "window", {
+ "type": "window",
+ "value": {
+ "context": any_string,
+ }
+ },
+ ),
+ ("new URL('https://example.com')", {"type": "object", },),
+]
+
+
+async def create_sandbox(bidi_session, context, sandbox_name="Test", method="evaluate"):
+ if method == "evaluate":
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ await_promise=False,
+ target=ContextTarget(context, sandbox=sandbox_name),
+ )
+ elif method == "call_function":
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => 1 + 2",
+ await_promise=False,
+ target=ContextTarget(context, sandbox=sandbox_name),
+ )
+ else:
+ raise Exception(f"Unsupported method to create a sandbox: {method}")
+
+ return result["realm"]
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/add_preload_script.py b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/add_preload_script.py
new file mode 100644
index 0000000000..cf5e77fac4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/add_preload_script.py
@@ -0,0 +1,172 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_add_preload_script(
+ bidi_session, add_preload_script, top_context, inline, type_hint
+):
+ await add_preload_script(function_declaration="() => { window.foo='bar'; }")
+
+ # Check that preload script didn't apply the changes to the current context
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "undefined"}
+
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ # Check that preload script applied the changes to the window
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "bar"}
+
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ # Check that preload script was applied after navigation
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "bar"}
+
+
+@pytest.mark.asyncio
+async def test_add_same_preload_script_twice(add_preload_script):
+ script_1 = await add_preload_script(function_declaration="() => { return 42; }")
+ script_2 = await add_preload_script(function_declaration="() => { return 42; }")
+
+ # Make sure that preload scripts have different ids
+ assert script_1 != script_2
+
+
+@pytest.mark.asyncio
+async def test_script_order(
+ bidi_session, add_preload_script, subscribe_events, new_tab, inline
+):
+ preload_script_console_text = "preload script"
+
+ await add_preload_script(
+ function_declaration=f"() => {{ console.log('{preload_script_console_text}') }}"
+ )
+ await subscribe_events(events=["log.entryAdded"], contexts=[new_tab["context"]])
+
+ events = []
+
+ async def on_event(method, data):
+ # Ignore errors and warnings which might occur during test execution
+ if data["level"] == "info":
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ user_console_text = "user script"
+ url = inline(f"<script>console.log('{user_console_text}')</script>")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete",
+ )
+
+ assert len(events) > 0
+ # Make sure that console event from preload script comes first
+ events[0]["text"] == preload_script_console_text
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_add_preload_script_in_iframe(
+ bidi_session, add_preload_script, new_tab, test_page_same_origin_frame
+):
+ await add_preload_script(function_declaration="() => { window.bar='foo'; }")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=test_page_same_origin_frame,
+ wait="complete",
+ )
+
+ # Check that preload script applied the changes to the window
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "foo"}
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+
+ assert len(contexts[0]["children"]) == 1
+ frame_context = contexts[0]["children"][0]
+
+ # Check that preload script applied the changes to the iframe
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(frame_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "foo"}
+
+
+@pytest.mark.asyncio
+async def test_add_preload_script_with_error(
+ bidi_session, add_preload_script, subscribe_events, inline, new_tab, wait_for_event, wait_for_future_safe
+):
+ await add_preload_script(
+ function_declaration="() => {{ throw Error('error in preload script') }}"
+ )
+
+ await subscribe_events(events=["browsingContext.load", "log.entryAdded"])
+
+ on_entry = wait_for_event("log.entryAdded")
+ on_load = wait_for_event("browsingContext.load")
+
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(context=new_tab["context"], url=url)
+ error_event = await wait_for_future_safe(on_entry)
+
+ # Make sure that page is loaded
+ await wait_for_future_safe(on_load)
+
+ # Make sure that exception from preloaded script was reported
+ assert error_event["level"] == "error"
+ assert error_event["text"] == "Error: error in preload script"
+
+
+@pytest.mark.asyncio
+async def test_page_script_can_access_preload_script_properties(
+ bidi_session, add_preload_script, new_tab, inline
+):
+ await add_preload_script(
+ function_declaration="() => { window.preloadScriptFunction = () => window.baz = 42; }"
+ )
+
+ url = inline("<script>window.preloadScriptFunction()</script>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete",
+ )
+
+ # Check that page script could access a function set up by the preload script
+ result = await bidi_session.script.evaluate(
+ expression="window.baz",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "number", "value": 42}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/arguments.py b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/arguments.py
new file mode 100644
index 0000000000..32f50b6991
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/arguments.py
@@ -0,0 +1,238 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import any_string, recursive_compare
+
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "channel, expected_data",
+ [
+ (
+ {"type": "channel", "value": {"channel": "channel_name"}},
+ {
+ "type": "object",
+ "value": [
+ ["foo", {"type": "string", "value": "bar"}],
+ [
+ "baz",
+ {
+ "type": "object",
+ "value": [["1", {"type": "number", "value": 2}]],
+ },
+ ],
+ ],
+ },
+ ),
+ (
+ {
+ "type": "channel",
+ "value": {
+ "channel": "channel_name",
+ "serializationOptions": {"maxObjectDepth": 0},
+ },
+ },
+ {"type": "object"},
+ ),
+ (
+ {
+ "type": "channel",
+ "value": {"channel": "channel_name", "ownership": "root"},
+ },
+ {
+ "handle": any_string,
+ "type": "object",
+ "value": [
+ ["foo", {"type": "string", "value": "bar"}],
+ [
+ "baz",
+ {
+ "type": "object",
+ "value": [["1", {"type": "number", "value": 2}]],
+ },
+ ],
+ ],
+ },
+ ),
+ ],
+ ids=["default", "with serializationOptions", "with ownership"],
+)
+async def test_channel(
+ bidi_session,
+ subscribe_events,
+ wait_for_event,
+ wait_for_future_safe,
+ add_preload_script,
+ channel,
+ expected_data,
+):
+ await subscribe_events(["script.message"])
+
+ on_script_message = wait_for_event("script.message")
+ await add_preload_script(
+ function_declaration="""(channel) => channel({'foo': 'bar', 'baz': {'1': 2}})""",
+ arguments=[channel],
+ )
+
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+ event_data = await wait_for_future_safe(on_script_message)
+
+ recursive_compare(
+ {
+ "channel": "channel_name",
+ "data": expected_data,
+ "source": {
+ "realm": any_string,
+ "context": new_tab["context"],
+ },
+ },
+ event_data,
+ )
+
+
+async def test_channel_with_multiple_arguments(
+ bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, add_preload_script
+):
+ await subscribe_events(["script.message"])
+
+ on_script_message = wait_for_event("script.message")
+ await add_preload_script(
+ function_declaration="""(channel) => channel('will_be_send', 'will_be_ignored')""",
+ arguments=[{"type": "channel", "value": {"channel": "channel_name"}}],
+ )
+
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+ event_data = await wait_for_future_safe(on_script_message)
+
+ recursive_compare(
+ {
+ "channel": "channel_name",
+ "data": {"type": "string", "value": "will_be_send"},
+ "source": {
+ "realm": any_string,
+ "context": new_tab["context"],
+ },
+ },
+ event_data,
+ )
+
+
+async def test_mutation_observer(
+ bidi_session,
+ subscribe_events,
+ wait_for_event,
+ wait_for_future_safe,
+ new_tab,
+ inline,
+ add_preload_script,
+):
+ await subscribe_events(["script.message"])
+
+ on_script_message = wait_for_event("script.message")
+ await add_preload_script(
+ function_declaration="""(channel) => {
+ const onMutation = (mutationList) => mutationList.forEach(mutation => {
+ const attributeName = mutation.attributeName;
+ const newValue = mutation.target.getAttribute(mutation.attributeName);
+ channel({ attributeName, newValue });
+ });
+ const observer = new MutationObserver(onMutation);
+ observer.observe(document, { attributes: true, subtree: true });
+ }""",
+ arguments=[{"type": "channel", "value": {"channel": "channel_name"}}],
+ )
+
+ url = inline("<div class='old class name'>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete",
+ )
+
+ restult = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="document.querySelector('div').setAttribute('class', 'mutated')",
+ await_promise=True,
+ target=ContextTarget(new_tab["context"]),
+ )
+
+ event_data = await wait_for_future_safe(on_script_message)
+
+ recursive_compare(
+ {
+ "channel": "channel_name",
+ "data": {
+ "type": "object",
+ "value": [
+ ["attributeName", {"type": "string", "value": "class"}],
+ ["newValue", {"type": "string", "value": "mutated"}],
+ ],
+ },
+ "source": {
+ "realm": restult["realm"],
+ "context": new_tab["context"],
+ },
+ },
+ event_data,
+ )
+
+
+async def test_two_channels(
+ bidi_session,
+ subscribe_events,
+ add_preload_script,
+):
+ await subscribe_events(["script.message"])
+
+ # Track all received script.message events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("script.message", on_event)
+
+ await add_preload_script(
+ function_declaration="""(channel_1, channel_2) => {
+ channel_1('message_from_channel_1');
+ channel_2('message_from_channel_2')
+ }""",
+ arguments=[
+ {"type": "channel", "value": {"channel": "channel_name_1"}},
+ {"type": "channel", "value": {"channel": "channel_name_2"}},
+ ],
+ )
+
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+ # Wait for both events
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ await wait.until(lambda _: len(events) == 2)
+
+ recursive_compare(
+ {
+ "channel": "channel_name_1",
+ "data": {"type": "string", "value": "message_from_channel_1"},
+ "source": {
+ "realm": any_string,
+ "context": new_tab["context"],
+ },
+ },
+ events[0],
+ )
+
+ recursive_compare(
+ {
+ "channel": "channel_name_2",
+ "data": {"type": "string", "value": "message_from_channel_2"},
+ "source": {
+ "realm": any_string,
+ "context": new_tab["context"],
+ },
+ },
+ events[1],
+ )
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/contexts.py b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/contexts.py
new file mode 100644
index 0000000000..135cfb6016
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/contexts.py
@@ -0,0 +1,111 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_top_context_with_iframes(
+ bidi_session, add_preload_script, new_tab,
+ inline, iframe, domain):
+
+ iframe_content = f"<div>{domain}</div>"
+ url = inline(f"{iframe(iframe_content, domain=domain)}")
+
+ await add_preload_script(
+ function_declaration="() => { window.bar='foo'; }",
+ contexts=[new_tab["context"]])
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete",
+ )
+
+ # Check that preload script applied the changes to the context
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "foo"}
+
+ contexts = await bidi_session.browsing_context.get_tree(
+ root=new_tab["context"])
+
+ assert len(contexts[0]["children"]) == 1
+ frame_context = contexts[0]["children"][0]
+
+ # Check that preload script applied the changes to the iframe
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(frame_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "foo"}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_page_script_context_isolation(bidi_session, add_preload_script,
+ top_context, type_hint,
+ test_page):
+ await add_preload_script(function_declaration="() => { window.baz = 42; }",
+ contexts=[top_context['context']])
+
+ new_context = await bidi_session.browsing_context.create(
+ type_hint=type_hint)
+
+ # Navigate both contexts to ensure preload script is triggered
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'],
+ url=test_page,
+ wait="complete",
+ )
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=test_page,
+ wait="complete",
+ )
+
+ # Check that preload script applied the changes to the context
+ result = await bidi_session.script.evaluate(
+ expression="window.baz",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "number", "value": 42}
+
+ # Check that preload script did *not* apply the changes to the other context
+ result = await bidi_session.script.evaluate(
+ expression="window.baz",
+ target=ContextTarget(new_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "undefined"}
+
+
+@pytest.mark.asyncio
+async def test_identical_contexts(
+ bidi_session, add_preload_script, new_tab,
+ inline):
+
+ url = inline(f"<div>test</div>")
+
+ await add_preload_script(
+ function_declaration="() => { window.foo = window.foo ? window.foo + 1 : 1; }",
+ contexts=[new_tab["context"], new_tab["context"]])
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete",
+ )
+
+ # Check that preload script applied the changes to the context only once
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "number", "value": 1}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/invalid.py
new file mode 100644
index 0000000000..46afcfbc8a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/invalid.py
@@ -0,0 +1,250 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("function_declaration", [None, False, 42, {}, []])
+async def test_params_function_declaration_invalid_type(
+ bidi_session, function_declaration
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration=function_declaration
+ ),
+
+
+@pytest.mark.parametrize("arguments", [False, "SOME_STRING", 42, {}])
+async def test_params_arguments_invalid_type(bidi_session, arguments):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=arguments,
+ )
+
+
+@pytest.mark.parametrize("argument", [False, "SOME_STRING", 42, {}, []])
+async def test_params_arguments_entry_invalid_type(bidi_session, argument):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[argument],
+ )
+
+
+async def test_params_arguments_entry_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[{"type": "foo"}],
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, "_UNKNOWN_", 42, []])
+async def test_params_arguments_channel_value_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[{"type": "channel", "value": value}],
+ )
+
+
+@pytest.mark.parametrize("channel", [None, False, 42, [], {}])
+async def test_params_arguments_channel_id_invalid_type(bidi_session, channel):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[{"type": "channel", "value": {"channel": channel}}],
+ )
+
+
+@pytest.mark.parametrize("ownership", [False, 42, {}, []])
+async def test_params_arguments_channel_ownership_invalid_type(bidi_session, ownership):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[{"type": "channel", "value": {"ownership": ownership}}],
+ )
+
+
+async def test_params_arguments_channel_ownership_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[{"type": "channel", "value": {
+ "ownership": "_UNKNOWN_"}}],
+ )
+
+
+@pytest.mark.parametrize("serialization_options", [False, "_UNKNOWN_", 42, []])
+async def test_params_arguments_channel_serialization_options_invalid_type(
+ bidi_session, serialization_options
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {"serializationOptions": serialization_options},
+ }
+ ],
+ )
+
+
+@pytest.mark.parametrize("max_dom_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_arguments_channel_max_dom_depth_invalid_type(
+ bidi_session, max_dom_depth
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {"serializationOptions": {"maxDomDepth": max_dom_depth}},
+ }
+ ],
+ )
+
+
+async def test_params_arguments_channel_max_dom_depth_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {"serializationOptions": {"maxDomDepth": -1}},
+ }
+ ],
+ )
+
+
+@pytest.mark.parametrize("max_object_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_arguments_channel_max_object_depth_invalid_type(
+ bidi_session, max_object_depth
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {"maxObjectDepth": max_object_depth}
+ },
+ }
+ ],
+ )
+
+
+async def test_params_arguments_channel_max_object_depth_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {"serializationOptions": {"maxObjectDepth": -1}},
+ }
+ ],
+ )
+
+
+@pytest.mark.parametrize("include_shadow_tree", [False, 42, {}, []])
+async def test_params_arguments_channel_include_shadow_tree_invalid_type(
+ bidi_session, include_shadow_tree
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {
+ "includeShadowTree": include_shadow_tree
+ }
+ },
+ }
+ ],
+ )
+
+
+async def test_params_arguments_channel_include_shadow_tree_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {"includeShadowTree": "_UNKNOWN_"}
+ },
+ }
+ ],
+ )
+
+
+@pytest.mark.parametrize("contexts", [False, 42, '_UNKNOWN_', {}])
+async def test_params_contexts_invalid_type(bidi_session, contexts):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ contexts=contexts
+ ),
+
+
+async def test_params_contexts_empty_list(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ contexts=[]
+ ),
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_contexts_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ contexts=[value]
+ ),
+
+
+@pytest.mark.parametrize("value", ["", "somestring"])
+async def test_params_contexts_context_invalid_value(bidi_session, value):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ contexts=[value]
+ ),
+
+
+async def test_params_contexts_context_non_top_level(bidi_session, new_tab, test_page_same_origin_frame):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=test_page_same_origin_frame,
+ wait="complete",
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+
+ assert len(contexts) == 1
+ assert len(contexts[0]["children"]) == 1
+ child_info = contexts[0]["children"][0]
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}",
+ contexts=[child_info['context']]
+ ),
+
+
+@pytest.mark.parametrize("sandbox", [False, 42, {}, []])
+async def test_params_sandbox_invalid_type(bidi_session, sandbox):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.add_preload_script(
+ function_declaration="() => {}", sandbox=sandbox
+ ),
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/sandbox.py b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/sandbox.py
new file mode 100644
index 0000000000..364eb5ce1a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/add_preload_script/sandbox.py
@@ -0,0 +1,70 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+async def test_add_preload_script_to_sandbox(bidi_session, add_preload_script):
+ # Add preload script to make changes in window
+ await add_preload_script(function_declaration="() => { window.foo = 1; }")
+ # Add preload script to make changes in sandbox
+ await add_preload_script(
+ function_declaration="() => { window.bar = 2; }", sandbox="sandbox"
+ )
+
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+
+ # Check that changes from the first preload script are not present in sandbox
+ result_in_sandbox = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+ assert result_in_sandbox == {"type": "undefined"}
+
+ # Make sure that changes from the second preload script are not present in window
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "undefined"}
+
+ # Make sure that changes from the second preload script are present in sandbox
+ result_in_sandbox = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+ assert result_in_sandbox == {"type": "number", "value": 2}
+
+
+@pytest.mark.asyncio
+async def test_remove_properties_set_by_preload_script(
+ bidi_session, add_preload_script, new_tab, inline
+):
+ await add_preload_script(function_declaration="() => { window.foo = 42 }")
+ await add_preload_script(function_declaration="() => { window.foo = 50 }", sandbox="sandbox_1")
+
+ url = inline("<script>delete window.foo</script>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url,
+ wait="complete",
+ )
+
+ # Check that page script could access a function set up by the preload script
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "undefined"}
+
+ # Check that page script could access a function set up by the preload script
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"], sandbox="sandbox_1"),
+ await_promise=True,
+ )
+ assert result == {"type": "number", "value": 50}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/arguments.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/arguments.py
new file mode 100644
index 0000000000..6d824befed
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/arguments.py
@@ -0,0 +1,101 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import recursive_compare
+from .. import PRIMITIVE_VALUES
+
+
+@pytest.mark.asyncio
+async def test_default_arguments(bidi_session, top_context):
+ result = await bidi_session.script.call_function(
+ function_declaration="(...args) => args",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare({"type": "array", "value": []}, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expected, argument", PRIMITIVE_VALUES)
+async def test_primitive_value(bidi_session, top_context, argument, expected):
+ result = await bidi_session.script.call_function(
+ function_declaration=f"""(arg) => {{
+ if (typeof {expected} === "number" && isNaN({expected})) {{
+ if (!isNaN(arg)) {{
+ throw new Error(`Argument should be {expected}, but was ` + arg);
+ }}
+ }} else if (arg !== {expected}) {{
+ throw new Error(`Argument should be {expected}, but was ` + arg);
+ }}
+ return arg;
+ }}""",
+ arguments=[argument],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(argument, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "argument, expected_type",
+ [
+ (
+ {
+ "type": "array",
+ "value": [
+ {"type": "string", "value": "foobar"},
+ ],
+ },
+ "Array",
+ ),
+ ({"type": "date", "value": "2022-05-31T13:47:29.000Z"}, "Date"),
+ (
+ {
+ "type": "map",
+ "value": [
+ ["foobar", {"type": "string", "value": "foobar"}],
+ ],
+ },
+ "Map",
+ ),
+ (
+ {
+ "type": "object",
+ "value": [
+ ["foobar", {"type": "string", "value": "foobar"}],
+ ],
+ },
+ "Object",
+ ),
+ ({"type": "regexp", "value": {"pattern": "foo", "flags": "g"}}, "RegExp"),
+ (
+ {
+ "type": "set",
+ "value": [
+ {"type": "string", "value": "foobar"},
+ ],
+ },
+ "Set",
+ ),
+ ],
+)
+async def test_local_value(bidi_session, top_context, argument, expected_type):
+ result = await bidi_session.script.call_function(
+ function_declaration=f"""(arg) => {{
+ if (!(arg instanceof {expected_type})) {{
+ const type = Object.prototype.toString.call(arg);
+ throw new Error(
+ "Argument type should be {expected_type}, but was " + type
+ );
+ }}
+ return arg;
+ }}""",
+ arguments=[argument],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(argument, result)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/await_promise.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/await_promise.py
new file mode 100644
index 0000000000..70ca469c11
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/await_promise.py
@@ -0,0 +1,46 @@
+import pytest
+
+from ... import recursive_compare
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_await_promise_delayed(bidi_session, top_context, await_promise):
+ result = await bidi_session.script.call_function(
+ function_declaration="""
+ async function() {{
+ await new Promise(r => setTimeout(() => r(), 0));
+ return "SOME_DELAYED_RESULT";
+ }}
+ """,
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ if await_promise:
+ assert result == {
+ "type": "string",
+ "value": "SOME_DELAYED_RESULT"}
+ else:
+ recursive_compare({
+ "type": "promise"},
+ result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_await_promise_async_arrow(bidi_session, top_context, await_promise):
+ result = await bidi_session.script.call_function(
+ function_declaration="async ()=>{return 'SOME_VALUE'}",
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]))
+
+ if await_promise:
+ assert result == {
+ "type": "string",
+ "value": "SOME_VALUE"}
+ else:
+ recursive_compare({
+ "type": "promise"},
+ result)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/channel.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/channel.py
new file mode 100644
index 0000000000..5bd92a4bfb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/channel.py
@@ -0,0 +1,217 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "channel, expected_data",
+ [
+ (
+ {"type": "channel", "value": {"channel": "channel_name"}},
+ {
+ "type": "object",
+ "value": [
+ ["foo", {"type": "string", "value": "bar"}],
+ [
+ "baz",
+ {
+ "type": "object",
+ "value": [["1", {"type": "number", "value": 2}]],
+ },
+ ],
+ ],
+ },
+ ),
+ (
+ {
+ "type": "channel",
+ "value": {
+ "channel": "channel_name",
+ "serializationOptions": {
+ "maxObjectDepth": 0
+ },
+ },
+ },
+ {"type": "object"},
+ ),
+ (
+ {
+ "type": "channel",
+ "value": {"channel": "channel_name", "ownership": "root"},
+ },
+ {
+ "handle": any_string,
+ "type": "object",
+ "value": [
+ ["foo", {"type": "string", "value": "bar"}],
+ [
+ "baz",
+ {
+ "type": "object",
+ "value": [["1", {"type": "number", "value": 2}]],
+ },
+ ],
+ ],
+ },
+ ),
+ ],
+ ids=["default", "with serializationOptions", "with ownership"],
+)
+async def test_channel(
+ bidi_session, top_context, subscribe_events, wait_for_event, wait_for_future_safe, channel, expected_data
+):
+ await subscribe_events(["script.message"])
+
+ on_script_message = wait_for_event("script.message")
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="""(channel) => channel({'foo': 'bar', 'baz': {'1': 2}})""",
+ arguments=[channel],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ event_data = await wait_for_future_safe(on_script_message)
+
+ recursive_compare(
+ {
+ "channel": "channel_name",
+ "data": expected_data,
+ "source": {
+ "realm": result["realm"],
+ "context": top_context["context"],
+ },
+ },
+ event_data,
+ )
+
+
+@pytest.mark.asyncio
+async def test_channel_with_multiple_arguments(
+ bidi_session, top_context, subscribe_events, wait_for_event, wait_for_future_safe
+):
+ await subscribe_events(["script.message"])
+
+ on_script_message = wait_for_event("script.message")
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="""(channel) => channel('will_be_send', 'will_be_ignored')""",
+ arguments=[{"type": "channel", "value": {"channel": "channel_name"}}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ event_data = await wait_for_future_safe(on_script_message)
+
+ recursive_compare(
+ {
+ "channel": "channel_name",
+ "data": {"type": "string", "value": "will_be_send"},
+ "source": {
+ "realm": result["realm"],
+ "context": top_context["context"],
+ },
+ },
+ event_data,
+ )
+
+
+@pytest.mark.asyncio
+async def test_two_channels(
+ bidi_session,
+ top_context,
+ subscribe_events,
+):
+ await subscribe_events(["script.message"])
+
+ # Track all received script.message events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("script.message", on_event)
+
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="""(channel_1, channel_2) => {
+ channel_1('message_from_channel_1');
+ channel_2('message_from_channel_2')
+ }""",
+ arguments=[
+ {"type": "channel", "value": {"channel": "channel_name_1"}},
+ {"type": "channel", "value": {"channel": "channel_name_2"}},
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ # Wait for both events
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ await wait.until(lambda _: len(events) == 2)
+
+ recursive_compare(
+ {
+ "channel": "channel_name_1",
+ "data": {"type": "string", "value": "message_from_channel_1"},
+ "source": {
+ "realm": result["realm"],
+ "context": top_context["context"],
+ },
+ },
+ events[0],
+ )
+
+ recursive_compare(
+ {
+ "channel": "channel_name_2",
+ "data": {"type": "string", "value": "message_from_channel_2"},
+ "source": {
+ "realm": result["realm"],
+ "context": top_context["context"],
+ },
+ },
+ events[1],
+ )
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_channel_and_nonchannel_arguments(
+ bidi_session,
+ top_context,
+ wait_for_event,
+ wait_for_future_safe,
+ subscribe_events,
+):
+ await subscribe_events(["script.message"])
+
+ on_script_message = wait_for_event("script.message")
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="""(string, channel) => {
+ channel(string);
+ }""",
+ arguments=[
+ {"type": "string", "value": "foo"},
+ {"type": "channel", "value": {"channel": "channel_name"}},
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ event_data = await wait_for_future_safe(on_script_message)
+
+ recursive_compare(
+ {
+ "channel": "channel_name",
+ "data": {"type": "string", "value": "foo"},
+ "source": {
+ "realm": result["realm"],
+ "context": top_context["context"],
+ },
+ },
+ event_data,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details.py
new file mode 100644
index 0000000000..25b27e407d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details.py
@@ -0,0 +1,86 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+
+from ... import any_int, any_string, recursive_compare
+from .. import any_stack_trace, PRIMITIVE_VALUES, REMOTE_VALUES
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expression, expected", PRIMITIVE_VALUES + REMOTE_VALUES)
+@pytest.mark.asyncio
+async def test_exception_details(bidi_session, top_context, expression, expected):
+ function_declaration = f"()=>{{ throw {expression} }}"
+
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": expected,
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
+
+
+@pytest.mark.asyncio
+async def test_invalid_function(bidi_session, top_context):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration="))) !!@@## some invalid JS script (((",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": {"type": "error"},
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("chained", [True, False])
+async def test_rejected_promise(bidi_session, top_context, chained):
+ if chained:
+ function_declaration = "() => Promise.reject('error').then(() => { })"
+ else:
+ function_declaration = "() => Promise.reject('error')"
+
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": {"type": "string", "value": "error"},
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details_await_promise.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details_await_promise.py
new file mode 100644
index 0000000000..6860158edc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/exception_details_await_promise.py
@@ -0,0 +1,33 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+
+from ... import any_int, any_string, recursive_compare
+from .. import any_stack_trace, PRIMITIVE_VALUES, REMOTE_VALUES
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expression, expected", PRIMITIVE_VALUES + REMOTE_VALUES)
+@pytest.mark.asyncio
+async def test_exception_details(bidi_session, top_context, expression, expected):
+ function_declaration = f"async()=>{{ throw {expression} }}"
+
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": expected,
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/function_declaration.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/function_declaration.py
new file mode 100644
index 0000000000..292e6da53b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/function_declaration.py
@@ -0,0 +1,14 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+async def test_arrow_function(bidi_session, top_context):
+ result = await bidi_session.script.call_function(
+ function_declaration="()=>{return 1+2;}",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {"type": "number", "value": 3}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/internal_id.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/internal_id.py
new file mode 100644
index 0000000000..562084203a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/internal_id.py
@@ -0,0 +1,67 @@
+import pytest
+
+from ... import recursive_compare, any_string
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "return_structure, result_type",
+ [
+ ("[data, data]", "array"),
+ ("new Map([['foo', data],['bar', data]])", "map"),
+ ("({ 'foo': data, 'bar': data })", "object"),
+ ],
+)
+@pytest.mark.parametrize(
+ "expression, type",
+ [
+ ("[1]", "array"),
+ ("new Map([[true, false]])", "map"),
+ ("new Set(['baz'])", "set"),
+ ("{ baz: 'qux' }", "object"),
+ ],
+)
+async def test_remote_values_with_internal_id(
+ call_function, return_structure, result_type, expression, type
+):
+ result = await call_function(
+ f"() => {{ const data = {expression}; return {return_structure}; }}"
+ )
+ result_value = result["value"]
+
+ assert len(result_value) == 2
+
+ if result_type == "array":
+ value = [
+ {"type": type, "internalId": any_string},
+ {"type": type, "internalId": any_string},
+ ]
+ internalId1 = result_value[0]["internalId"]
+ internalId2 = result_value[1]["internalId"]
+ else:
+ value = [
+ ["foo", {"type": type, "internalId": any_string}],
+ ["bar", {"type": type, "internalId": any_string}],
+ ]
+ internalId1 = result_value[0][1]["internalId"]
+ internalId2 = result_value[1][1]["internalId"]
+
+ # Make sure that the same duplicated objects have the same internal ids
+ assert internalId1 == internalId2
+
+ recursive_compare(value, result_value)
+
+
+@pytest.mark.asyncio
+async def test_different_remote_values_have_unique_internal_ids(call_function):
+ result = await call_function(
+ "() => { const obj1 = [1]; const obj2 = {'foo': 'bar'}; return [obj1, obj2, obj1, obj2]; }"
+ )
+
+ assert len(result["value"]) == 4
+
+ internalId1 = result["value"][0]["internalId"]
+ internalId2 = result["value"][1]["internalId"]
+
+ # Make sure that different duplicated objects have different internal ids
+ assert internalId1 != internalId2
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/invalid.py
new file mode 100644
index 0000000000..af94e86efe
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/invalid.py
@@ -0,0 +1,433 @@
+import pytest
+import webdriver.bidi.error as error
+
+from webdriver.bidi.modules.script import ContextTarget, RealmTarget, SerializationOptions
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("target", [None, False, "foo", 42, {}, []])
+async def test_params_target_invalid_type(bidi_session, target):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=False,
+ target=target)
+
+
+@pytest.mark.parametrize("context", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=False,
+ target=ContextTarget(context))
+
+
+@pytest.mark.parametrize("sandbox", [False, 42, {}, []])
+async def test_params_sandbox_invalid_type(bidi_session, top_context, sandbox):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=False,
+ target=ContextTarget(top_context["context"],
+ sandbox))
+
+
+async def test_params_context_unknown(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=False,
+ target=ContextTarget("_UNKNOWN_"))
+
+
+@pytest.mark.parametrize("realm", [None, False, 42, {}, []])
+async def test_params_realm_invalid_type(bidi_session, realm):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=False,
+ target=RealmTarget(realm))
+
+
+async def test_params_realm_unknown(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=False,
+ target=RealmTarget("_UNKNOWN_"))
+
+
+@pytest.mark.parametrize("function_declaration", [None, False, 42, {}, []])
+async def test_params_function_declaration_invalid_type(bidi_session, top_context,
+ function_declaration):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("this", [False, "SOME_STRING", 42, {}, []])
+async def test_params_this_invalid_type(bidi_session, top_context,
+ this):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ this=this,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("arguments", [False, "SOME_STRING", 42, {}])
+async def test_params_arguments_invalid_type(bidi_session, top_context,
+ arguments):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=arguments,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("argument", [False, "SOME_STRING", 42, {}, []])
+async def test_params_arguments_entry_invalid_type(bidi_session, top_context,
+ argument):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[argument],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("value", [None, False, "_UNKNOWN_", 42, []])
+async def test_params_arguments_channel_value_invalid_type(
+ bidi_session, top_context, value
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[{"type": "channel", "value": value}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.parametrize("channel", [None, False, 42, [], {}])
+async def test_params_arguments_channel_id_invalid_type(
+ bidi_session, top_context, channel
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[{"type": "channel", "value": {"channel": channel}}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.parametrize("ownership", [False, 42, {}, []])
+async def test_params_arguments_channel_ownership_invalid_type(
+ bidi_session, top_context, ownership
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[{"type": "channel", "value": {"ownership": ownership}}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+async def test_params_arguments_channel_ownership_invalid_value(
+ bidi_session, top_context
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[{"type": "channel", "value": {"ownership": "_UNKNOWN_"}}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.parametrize("serialization_options", [False, "_UNKNOWN_", 42, []])
+async def test_params_arguments_channel_serialization_options_invalid_type(
+ bidi_session, top_context, serialization_options
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {"serializationOptions": serialization_options},
+ }
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.parametrize("max_dom_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_arguments_channel_max_dom_depth_invalid_type(
+ bidi_session, top_context, max_dom_depth
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {"maxDomDepth": max_dom_depth}
+ },
+ }
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+async def test_params_arguments_channel_max_dom_depth_invalid_value(
+ bidi_session, top_context
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {"maxDomDepth": -1}
+ },
+ }
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.parametrize("max_object_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_arguments_channel_max_object_depth_invalid_type(
+ bidi_session, top_context, max_object_depth
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {"maxObjectDepth": max_object_depth}
+ },
+ }
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+async def test_params_arguments_channel_max_object_depth_invalid_value(
+ bidi_session, top_context
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {"serializationOptions": {"maxObjectDepth": -1}},
+ }
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.parametrize("include_shadow_tree", [False, 42, {}, []])
+async def test_params_arguments_channel_include_shadow_tree_invalid_type(
+ bidi_session, top_context, include_shadow_tree
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {
+ "includeShadowTree": include_shadow_tree
+ }
+ },
+ }
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+async def test_params_arguments_channel_include_shadow_tree_invalid_value(
+ bidi_session, top_context
+):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[
+ {
+ "type": "channel",
+ "value": {
+ "serializationOptions": {"includeShadowTree": "_UNKNOWN_"}
+ },
+ }
+ ],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_arguments_handle_invalid_type(
+ bidi_session, top_context, value
+):
+ serialized_value = {
+ "handle": value,
+ }
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[serialized_value],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+
+async def test_params_arguments_handle_unknown_value(
+ bidi_session, top_context
+):
+ serialized_value = {
+ "handle": "foo",
+ }
+
+ with pytest.raises(error.NoSuchHandleException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[serialized_value],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_arguments_sharedId_invalid_type(
+ bidi_session, top_context, value
+):
+ serialized_value = {
+ "sharedId": value,
+ }
+
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ arguments=[serialized_value],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("await_promise", [None, "False", 0, 42, {}, []])
+async def test_params_await_promise_invalid_type(bidi_session, top_context,
+ await_promise):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("result_ownership", [False, "_UNKNOWN_", 42, {}, []])
+async def test_params_result_ownership_invalid_value(bidi_session, top_context,
+ result_ownership):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ result_ownership=result_ownership)
+
+
+@pytest.mark.parametrize("serialization_options", [False, "_UNKNOWN_", 42, []])
+async def test_params_serialization_options_invalid_type(bidi_session, top_context, serialization_options):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ serialization_options=serialization_options,
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("max_dom_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_max_dom_depth_invalid_type(bidi_session, top_context, max_dom_depth):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ serialization_options=SerializationOptions(max_dom_depth=max_dom_depth),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+async def test_params_max_dom_depth_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ serialization_options=SerializationOptions(max_dom_depth=-1),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("max_object_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_max_object_depth_invalid_type(bidi_session, top_context, max_object_depth):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ serialization_options=SerializationOptions(max_object_depth=max_object_depth),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+async def test_params_max_object_depth_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ serialization_options=SerializationOptions(max_object_depth=-1),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("include_shadow_tree", [False, 42, {}, []])
+async def test_params_include_shadow_tree_invalid_type(bidi_session, top_context, include_shadow_tree):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ serialization_options=SerializationOptions(include_shadow_tree=include_shadow_tree),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+async def test_params_include_shadow_tree_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ serialization_options=SerializationOptions(include_shadow_tree="foo"),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("user_activation", ["foo", 42, {}, []])
+async def test_params_user_activation_invalid_type(bidi_session, top_context, user_activation):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.call_function(
+ function_declaration="(arg) => arg",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ user_activation=user_activation)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/primitive_values.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/primitive_values.py
new file mode 100644
index 0000000000..d7cdd78b9c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/primitive_values.py
@@ -0,0 +1,22 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+from .. import PRIMITIVE_VALUES
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+@pytest.mark.parametrize("expression, expected", PRIMITIVE_VALUES)
+async def test_primitive_values(bidi_session, top_context, expression,
+ expected, await_promise):
+ function_declaration = f"()=>{expression}"
+ if await_promise:
+ function_declaration = "async" + function_declaration
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == expected
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/realm.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/realm.py
new file mode 100644
index 0000000000..a8830230ee
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/realm.py
@@ -0,0 +1,71 @@
+import pytest
+
+from webdriver.bidi.modules.script import RealmTarget
+from ... import recursive_compare
+
+
+@pytest.mark.asyncio
+async def test_target_realm(bidi_session, default_realm):
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => { window.foo = 3; }",
+ target=RealmTarget(default_realm),
+ await_promise=True,
+ )
+
+ recursive_compare({"realm": default_realm, "result": {"type": "undefined"}}, result)
+
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => window.foo",
+ target=RealmTarget(default_realm),
+ await_promise=True,
+ )
+
+ recursive_compare(
+ {"realm": default_realm, "result": {"type": "number", "value": 3}}, result
+ )
+
+
+@pytest.mark.asyncio
+async def test_different_target_realm(bidi_session):
+ await bidi_session.browsing_context.create(type_hint="tab")
+
+ realms = await bidi_session.script.get_realms()
+ first_tab_default_realm = realms[0]["realm"]
+ second_tab_default_realm = realms[1]["realm"]
+
+ assert first_tab_default_realm != second_tab_default_realm
+
+ await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => { window.foo = 3; }",
+ target=RealmTarget(first_tab_default_realm),
+ await_promise=True,
+ )
+ await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => { window.foo = 5; }",
+ target=RealmTarget(second_tab_default_realm),
+ await_promise=True,
+ )
+
+ top_context_result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => window.foo",
+ target=RealmTarget(first_tab_default_realm),
+ await_promise=True,
+ )
+ recursive_compare(
+ {"realm": first_tab_default_realm, "result": {"type": "number", "value": 3}}, top_context_result
+ )
+
+ new_context_result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => window.foo",
+ target=RealmTarget(second_tab_default_realm),
+ await_promise=True,
+ )
+ recursive_compare(
+ {"realm": second_tab_default_realm, "result": {"type": "number", "value": 5}}, new_context_result
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_reference.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_reference.py
new file mode 100644
index 0000000000..8bf055e34d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_reference.py
@@ -0,0 +1,338 @@
+import pytest
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.script import ContextTarget, SerializationOptions
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "setup_expression, function_declaration, expected",
+ [
+ (
+ "Symbol('foo')",
+ "(symbol) => symbol.toString()",
+ {"type": "string", "value": "Symbol(foo)"},
+ ),
+ ("[1,2]", "(array) => array[0]", {"type": "number", "value": 1}),
+ (
+ "new RegExp('foo')",
+ "(regexp) => regexp.source",
+ {"type": "string", "value": "foo"},
+ ),
+ (
+ "new Date(1654004849000)",
+ "(date) => date.toISOString()",
+ {"type": "string", "value": "2022-05-31T13:47:29.000Z"},
+ ),
+ (
+ "new Map([['foo', 'bar']])",
+ "(map) => map.get('foo')",
+ {"type": "string", "value": "bar"},
+ ),
+ (
+ "new Set(['foo'])",
+ "(set) => set.has('foo')",
+ {"type": "boolean", "value": True},
+ ),
+ (
+ "{const weakMap = new WeakMap(); weakMap.set(weakMap, 'foo')}",
+ "(weakMap)=> weakMap.get(weakMap)",
+ {"type": "string", "value": "foo"},
+ ),
+ (
+ "{const weakSet = new WeakSet(); weakSet.add(weakSet)}",
+ "(weakSet)=> weakSet.has(weakSet)",
+ {"type": "boolean", "value": True},
+ ),
+ (
+ "new Error('error message')",
+ "(error) => error.message",
+ {"type": "string", "value": "error message"},
+ ),
+ (
+ "new SyntaxError('syntax error message')",
+ "(error) => error.message",
+ {"type": "string", "value": "syntax error message"},
+ ),
+ (
+ "new Promise((resolve) => resolve(3))",
+ "(promise) => promise",
+ {"type": "number", "value": 3},
+ ),
+ (
+ "new Int8Array(2)",
+ "(int8Array) => int8Array.length",
+ {"type": "number", "value": 2},
+ ),
+ (
+ "new ArrayBuffer(8)",
+ "(arrayBuffer) => arrayBuffer.byteLength",
+ {"type": "number", "value": 8},
+ ),
+ ("() => true", "(func) => func()", {"type": "boolean", "value": True}),
+ (
+ "(function() {return false;})",
+ "(func) => func()",
+ {"type": "boolean", "value": False},
+ ),
+ (
+ "window.foo = 3; window",
+ "(window) => window.foo",
+ {"type": "number", "value": 3},
+ ),
+ (
+ "window.url = new URL('https://example.com'); window.url",
+ "(url) => url.hostname",
+ {"type": "string", "value": "example.com"},
+ ),
+ (
+ "({SOME_PROPERTY:'SOME_VALUE'})",
+ "(obj) => obj.SOME_PROPERTY",
+ {"type": "string", "value": "SOME_VALUE"},
+ ),
+ ],
+)
+async def test_remote_reference_argument(
+ bidi_session, top_context, setup_expression, function_declaration, expected
+):
+ remote_value_result = await bidi_session.script.evaluate(
+ expression=setup_expression,
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+ remote_value_handle = remote_value_result.get("handle")
+
+ assert isinstance(remote_value_handle, str)
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ arguments=[{"handle": remote_value_handle}],
+ await_promise=True if remote_value_result["type"] == "promise" else False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == expected
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "value_fn, function_declaration",
+ [
+ (
+ lambda value: value,
+ "function(arg) { return arg === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "object", "value": [["nested", value]]}),
+ "function(arg) { return arg.nested === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "array", "value": [value]}),
+ "function(arg) { return arg[0] === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "map", "value": [["foobar", value]]}),
+ "function(arg) { return arg.get('foobar') === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "set", "value": [value]}),
+ "function(arg) { return arg.has(window.SOME_OBJECT); }",
+ ),
+ ],
+)
+async def test_remote_reference_deserialization(
+ bidi_session, top_context, call_function, evaluate, value_fn, function_declaration
+):
+ remote_value = await evaluate(
+ "window.SOME_OBJECT = { SOME_PROPERTY: 'SOME_VALUE' }; window.SOME_OBJECT",
+ result_ownership="root",
+ )
+
+ # Check that a remote value can be successfully deserialized as an "argument"
+ # parameter and compared against the original object in the page.
+ result = await call_function(
+ function_declaration=function_declaration,
+ arguments=[value_fn(remote_value)],
+ )
+ assert result == {"type": "boolean", "value": True}
+
+ # Reload the page to cleanup the state
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=top_context["url"], wait="complete"
+ )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "setup_expression, expected_node_type",
+ [
+ ("document.querySelector('img')", 1),
+ ("document.querySelector('input#button').attributes[0]", 2),
+ ("document.querySelector('#with-text-node').childNodes[0]", 3),
+ ("""document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")""", 7),
+ ("document.querySelector('#with-comment').childNodes[0]", 8),
+ ("document", 9),
+ ("document.doctype", 10),
+ ("document.createDocumentFragment()", 11),
+ ("document.querySelector('#custom-element').shadowRoot", 11),
+ ],
+ ids=[
+ "element",
+ "attribute",
+ "text node",
+ "processing instruction",
+ "comment",
+ "document",
+ "doctype",
+ "document fragment",
+ "shadow root",
+ ]
+)
+async def test_remote_reference_node_argument(
+ bidi_session, get_test_page, top_context, setup_expression, expected_node_type
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ remote_reference = await bidi_session.script.evaluate(
+ expression=setup_expression,
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration="(node) => node.nodeType",
+ arguments=[remote_reference],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {"type": "number", "value": expected_node_type}
+
+
+@pytest.mark.asyncio
+async def test_remote_reference_node_cdata(bidi_session, inline, top_context):
+ xml_page = inline("""<foo>CDATA section: <![CDATA[ < > & ]]>.</foo>""", doctype="xml")
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=xml_page, wait="complete"
+ )
+
+ remote_reference = await bidi_session.script.evaluate(
+ expression="document.querySelector('foo').childNodes[1]",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration="(node) => node.nodeType",
+ arguments=[remote_reference],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {"type": "number", "value": 4}
+
+
+@pytest.mark.asyncio
+async def test_remote_reference_sharedId_precedence_over_handle(
+ bidi_session, get_test_page, top_context
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ remote_reference = await bidi_session.script.evaluate(
+ expression="document.querySelector('img')",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert "handle" in remote_reference
+ # Invalidate shared reference to trigger a "no such node" error
+ remote_reference["sharedId"] = "foo"
+
+ with pytest.raises(error.NoSuchNodeException):
+ await bidi_session.script.call_function(
+ function_declaration="(node) => node.nodeType",
+ arguments=[remote_reference],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, function_declaration, expected",
+ [
+ (
+ "document.getElementsByTagName('span')",
+ "(collection) => collection.item(0)",
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }
+ ),
+ (
+ "document.querySelectorAll('span')",
+ "(nodeList) => nodeList.item(0)",
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }
+ ),
+ ], ids=[
+ "htmlcollection",
+ "nodelist"
+ ]
+)
+async def test_remote_reference_dom_collection(
+ bidi_session,
+ inline,
+ top_context,
+ call_function,
+ expression,
+ function_declaration,
+ expected
+):
+ page_url = inline("""<p><span>""")
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=page_url, wait="complete"
+ )
+
+ remote_value = await bidi_session.script.evaluate(
+ expression=expression,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ # Check that a remote value can be successfully deserialized as an "argument"
+ # parameter and the first element be extracted.
+ result = await call_function(
+ function_declaration=function_declaration,
+ arguments=[remote_value],
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_values.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_values.py
new file mode 100644
index 0000000000..b9399662cd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/remote_values.py
@@ -0,0 +1,179 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, SerializationOptions
+from ... import recursive_compare
+from .. import REMOTE_VALUES
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("await_promise", [True, False])
+@pytest.mark.parametrize("expression, expected", [
+ remote_value
+ for remote_value in REMOTE_VALUES if remote_value[1]["type"] != "promise"
+])
+async def test_remote_values(bidi_session, top_context, await_promise,
+ expression, expected):
+ function_declaration = f"()=>{expression}"
+ if await_promise:
+ function_declaration = "async" + function_declaration
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ serialization_options=SerializationOptions(max_object_depth=1),
+ )
+ recursive_compare(expected, result)
+
+
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_remote_value_promise(bidi_session, top_context, await_promise):
+ result = await bidi_session.script.call_function(
+ function_declaration="()=>Promise.resolve(42)",
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ if await_promise:
+ assert result == {"type": "number", "value": 42}
+ else:
+ assert result == {"type": "promise"}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_window_context_top_level(bidi_session, top_context,
+ await_promise):
+ function_declaration = "() => window"
+ if await_promise:
+ function_declaration = "async" + function_declaration
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(
+ {
+ "type": "window",
+ "value": {
+ "context": top_context["context"]
+ }
+ }, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("domain", ["", "alt"],
+ ids=["same_origin", "cross_origin"])
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_window_context_iframe_window(bidi_session, top_context,
+ inline, domain, await_promise):
+
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ all_contexts = await bidi_session.browsing_context.get_tree()
+ iframe_context = all_contexts[0]["children"][0]
+
+ function_declaration = "() => window"
+ if await_promise:
+ function_declaration = "async" + function_declaration
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(iframe_context["context"]),
+ )
+
+ recursive_compare(
+ {
+ "type": "window",
+ "value": {
+ "context": iframe_context["context"]
+ }
+ }, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("domain", ["", "alt"],
+ ids=["same_origin", "cross_origin"])
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_window_context_iframe_content_window(
+ bidi_session, top_context, inline, domain, await_promise):
+
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ all_contexts = await bidi_session.browsing_context.get_tree()
+ iframe_context = all_contexts[0]["children"][0]
+
+ # This is equivalent to `document.getElementsByTagName("iframe")[0].contentWindow`
+ function_declaration = "() => window.frames[0]"
+ if await_promise:
+ function_declaration = "async" + function_declaration
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(
+ {
+ "type": "window",
+ "value": {
+ "context": iframe_context["context"]
+ }
+ }, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+@pytest.mark.parametrize("domain", ["", "alt"],
+ ids=["same_origin", "cross_origin"])
+async def test_window_context_same_id_after_navigation(bidi_session,
+ top_context, inline,
+ await_promise, domain):
+
+ defaultOrigin = inline(f"{domain}")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=defaultOrigin, wait="complete")
+
+ url = inline(f"{domain}", domain=domain)
+
+ function_declaration = "() => window"
+ if await_promise:
+ function_declaration = "async" + function_declaration
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ original_context_id = result['value']['context']
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete")
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ navigated_context_id = result['value']['context']
+
+ assert navigated_context_id == original_context_id
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_node.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_node.py
new file mode 100644
index 0000000000..47cbd42d22
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_node.py
@@ -0,0 +1,759 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, SerializationOptions
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ ( # basic
+ """
+ () => document.querySelector("br")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # attributes
+ """
+ () => document.querySelector("svg")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {
+ "svg:foo": "bar",
+ },
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "svg",
+ "namespaceURI": "http://www.w3.org/2000/svg",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # all children including non-element nodes
+ """
+ () => document.querySelector("#with-text-node")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-text-node"},
+ "childNodeCount": 1,
+ "children": [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 3,
+ "nodeValue": "Lorem",
+ }
+ }],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # children limited due to max depth
+ """
+ () => document.querySelector("#with-children")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "children": [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }, {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # not connected
+ """
+ () => document.createElement("div")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ], ids=[
+ "basic",
+ "attributes",
+ "all_children",
+ "children_max_depth",
+ "not_connected",
+ ]
+)
+async def test_element_node(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document.querySelector("input#button").attributes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "localName": "id",
+ "namespaceURI": None,
+ "nodeType": 2,
+ "nodeValue": "button",
+ },
+ },
+ ), (
+ """
+ () => document.querySelector("svg").attributes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "localName": "foo",
+ "namespaceURI": "http://www.w3.org/2000/svg",
+ "nodeType": 2,
+ "nodeValue": "bar",
+ },
+ },
+ ),
+ ], ids=[
+ "basic",
+ "namespace",
+ ]
+)
+async def test_attribute_node(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document.querySelector("#with-text-node").childNodes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 3,
+ "nodeValue": "Lorem",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_text_node(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document.querySelector("foo").childNodes[1]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 4,
+ "nodeValue": " < > & ",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_cdata_node(bidi_session, inline, new_tab, function_declaration, expected):
+ xml_page = inline("""<foo>CDATA section: <![CDATA[ < > & ]]>.</foo>""", doctype="xml")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab['context'], url=xml_page, wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 7,
+ "nodeValue": "href='foo.css'",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_processing_instruction_node(
+ bidi_session, inline, new_tab, function_declaration, expected
+):
+ xml_page = inline("""<foo></foo>""", doctype="xml")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab['context'], url=xml_page, wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document.querySelector("#with-comment").childNodes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 8,
+ "nodeValue": " Comment ",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_comment_node(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 2,
+ "children": [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 10
+ }
+ }, {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 2,
+ "localName": "html",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }],
+ "nodeType": 9
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_document_node(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document.doctype
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 10,
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_doctype_node(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => document.querySelector("#custom-element").shadowRoot
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 1,
+ "mode": "open",
+ "nodeType": 11
+ }
+ }
+ ),
+ (
+ """
+ () => document.createDocumentFragment()
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "children": [],
+ "nodeType": 11,
+ }
+ }
+ ),
+ ], ids=[
+ "shadow root",
+ "not connected",
+ ]
+)
+async def test_document_fragment_node(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ """
+ () => [document.querySelector("img")]
+ """,
+ {
+ "type": "array",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ],
+ },
+ ),
+ (
+ """
+ () => {
+ const map = new Map();
+ map.set(document.querySelector("img"), "elem");
+ return map;
+ }
+ """,
+ {
+ "type": "map",
+ "value": [[
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ },
+ {
+ "type": "string",
+ "value": "elem"
+ }
+ ]]
+ }
+ ),
+ (
+ """
+ () => {
+ const map = new Map();
+ map.set("elem", document.querySelector("img"));
+ return map;
+ }
+ """,
+ {
+ "type": "map",
+ "value": [[
+ "elem", {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }
+ ]]
+ }
+ ),
+ (
+ """
+ () => ({"elem": document.querySelector("img")})
+ """,
+ {
+ "type": "object",
+ "value": [
+ ["elem", {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }]
+ ]
+ }
+ ),
+ (
+ """
+ () => {
+ const set = new Set();
+ set.add(document.querySelector("img"));
+ return set;
+ }
+ """,
+ {
+ "type": "set",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ],
+ },
+ ),
+ ], ids=[
+ "array", "map-key", "map-value", "object", "set"
+ ]
+)
+async def test_node_embedded_within(
+ bidi_session, get_test_page, top_context, function_declaration, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "function_declaration, expected",
+ [
+ (
+ "() => document.getElementsByTagName('img')",
+ {
+ "type": "htmlcollection",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ },
+ ]
+ }
+ ),
+ (
+ "() => document.querySelectorAll('img')",
+ {
+ "type": "nodelist",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ },
+ ]
+ }
+ ),
+ ], ids=[
+ "htmlcollection",
+ "nodelist"
+ ]
+)
+async def test_node_within_dom_collection(
+ bidi_session,
+ get_test_page,
+ top_context,
+ function_declaration,
+ expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.parametrize("shadow_root_mode", ["open", "closed"])
+@pytest.mark.asyncio
+async def test_custom_element_with_shadow_root(
+ bidi_session, get_test_page, top_context, shadow_root_mode
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(shadow_root_mode=shadow_root_mode),
+ wait="complete",
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => document.querySelector("#custom-element")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare({
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {
+ "id": "custom-element",
+ },
+ "childNodeCount": 0,
+ "localName": "custom-element",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "childNodeCount": 1,
+ "mode": shadow_root_mode,
+ "nodeType": 11,
+ }
+ },
+ }
+ }, result)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_ownership.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_ownership.py
new file mode 100644
index 0000000000..6a96f87ad1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/result_ownership.py
@@ -0,0 +1,60 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+from ... import assert_handle
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_throw_exception(bidi_session, top_context, result_ownership, should_contain_handle):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration='()=>{throw {a:1}}',
+ await_promise=False,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(exception.value.result["exceptionDetails"]["exception"], should_contain_handle)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_invalid_script(bidi_session, top_context, result_ownership, should_contain_handle):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration="))) !!@@## some invalid JS script (((",
+ await_promise=False,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(exception.value.result["exceptionDetails"]["exception"], should_contain_handle)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_rejected_promise(bidi_session, top_context, result_ownership, should_contain_handle):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration="()=>{return Promise.reject({a:1})}",
+ await_promise=True,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(exception.value.result["exceptionDetails"]["exception"], should_contain_handle)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_return_value(bidi_session, top_context, await_promise, result_ownership, should_contain_handle):
+ result = await bidi_session.script.call_function(
+ function_declaration="async function(){return {a: {b:1}}}",
+ await_promise=await_promise,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(result, should_contain_handle)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/sandbox.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/sandbox.py
new file mode 100644
index 0000000000..382ede3c78
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/sandbox.py
@@ -0,0 +1,239 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, RealmTarget, ScriptEvaluateResultException
+
+from ... import any_int, any_string, recursive_compare
+from .. import any_stack_trace
+
+
+@pytest.mark.asyncio
+async def test_sandbox(bidi_session, new_tab):
+ # Make changes in window
+ await bidi_session.script.call_function(
+ function_declaration="() => { window.foo = 1; }",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+
+ # Check that changes are not present in sandbox
+ result_in_sandbox = await bidi_session.script.call_function(
+ function_declaration="() => window.foo",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+ assert result_in_sandbox == {"type": "undefined"}
+
+ # Make changes in sandbox
+ await bidi_session.script.call_function(
+ function_declaration="() => { window.bar = 2; }",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+
+ # Make sure that changes are present in sandbox
+ result_in_sandbox = await bidi_session.script.call_function(
+ function_declaration="() => window.bar",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+ assert result_in_sandbox == {"type": "number", "value": 2}
+
+ # Make sure that changes didn't leak from sandbox
+ result_in_window = await bidi_session.script.call_function(
+ function_declaration="() => window.bar",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result_in_window == {"type": "undefined"}
+
+
+@pytest.mark.asyncio
+async def test_sandbox_with_empty_name(bidi_session, new_tab):
+ # An empty string as a `sandbox` means the default realm should be used.
+ await bidi_session.script.call_function(
+ function_declaration="() => window.foo = 'bar'",
+ target=ContextTarget(new_tab["context"], ""),
+ await_promise=True,
+ )
+
+ # Make sure that we can find the sandbox with the empty name.
+ result = await bidi_session.script.call_function(
+ function_declaration="() => window.foo",
+ target=ContextTarget(new_tab["context"], ""),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "bar"}
+
+ # Make sure that we can find the value in the default realm.
+ result = await bidi_session.script.call_function(
+ function_declaration="() => window.foo",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "bar"}
+
+
+@pytest.mark.asyncio
+async def test_switch_sandboxes(bidi_session, new_tab):
+ # Test that sandboxes are retained when switching between them
+ await bidi_session.script.call_function(
+ function_declaration="() => { window.foo = 1; }",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ await bidi_session.script.call_function(
+ function_declaration="() => { window.foo = 2; }",
+ target=ContextTarget(new_tab["context"], "sandbox_2"),
+ await_promise=True,
+ )
+
+ result_in_sandbox_1 = await bidi_session.script.call_function(
+ function_declaration="() => window.foo",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_1 == {"type": "number", "value": 1}
+
+ result_in_sandbox_2 = await bidi_session.script.call_function(
+ function_declaration="() => window.foo",
+ target=ContextTarget(new_tab["context"], "sandbox_2"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_2 == {"type": "number", "value": 2}
+
+
+@pytest.mark.asyncio
+async def test_sandbox_with_side_effects(bidi_session, new_tab):
+ # Make sure changing the node in sandbox will affect the other sandbox as well
+ await bidi_session.script.call_function(
+ function_declaration="() => document.querySelector('body').textContent = 'foo'",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ expected_value = {"type": "string", "value": "foo"}
+
+ result_in_sandbox_1 = await bidi_session.script.call_function(
+ function_declaration="() => document.querySelector('body').textContent",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_1 == expected_value
+
+ result_in_sandbox_2 = await bidi_session.script.call_function(
+ function_declaration="() => document.querySelector('body').textContent",
+ target=ContextTarget(new_tab["context"], "sandbox_2"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_2 == expected_value
+
+
+@pytest.mark.asyncio
+async def test_sandbox_returns_same_node(bidi_session, new_tab):
+ node = await bidi_session.script.call_function(
+ function_declaration="() => document.querySelector('body')",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ recursive_compare({"type": "node", "sharedId": any_string}, node)
+
+ node_sandbox = await bidi_session.script.call_function(
+ function_declaration="() => document.querySelector('body')",
+ target=ContextTarget(new_tab["context"], sandbox="sandbox_1"),
+ await_promise=True,
+ )
+ assert node_sandbox == node
+
+
+@pytest.mark.asyncio
+async def test_arguments(bidi_session, new_tab):
+ argument = {
+ "type": "set",
+ "value": [
+ {"type": "string", "value": "foobar"},
+ ],
+ }
+
+ result = await bidi_session.script.call_function(
+ function_declaration="""(arg) => {
+ if(! (arg instanceof Set))
+ throw Error("Argument type should be Set, but was "+
+ Object.prototype.toString.call(arg));
+ return arg;
+ }""",
+ arguments=[argument],
+ await_promise=False,
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ )
+ recursive_compare(argument, result)
+
+
+@pytest.mark.asyncio
+async def test_arguments_uses_same_node_in_sandbox(bidi_session, new_tab):
+ node = await bidi_session.script.call_function(
+ function_declaration="() => document.querySelector('body')",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ recursive_compare({"type": "node", "sharedId": any_string}, node)
+
+ result = await bidi_session.script.call_function(
+ function_declaration="""(node) => node.localName""",
+ arguments=[node],
+ await_promise=False,
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ )
+ assert result == {"type": "string", "value": "body"}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_exception_details(bidi_session, new_tab, await_promise):
+ function_declaration = "()=>{{ throw 1 }}"
+ if await_promise:
+ function_declaration = "async" + function_declaration
+
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ await_promise=await_promise,
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": {"type": "number", "value": 1},
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
+
+
+@pytest.mark.asyncio
+async def test_target_realm(bidi_session, top_context, default_realm):
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => { window.foo = 3; }",
+ target=ContextTarget(top_context["context"], "sandbox"),
+ await_promise=True,
+ )
+ realm = result["realm"]
+
+ # Make sure that sandbox realm id is different from default
+ assert realm != default_realm
+
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => window.foo",
+ target=RealmTarget(realm),
+ await_promise=True,
+ )
+
+ recursive_compare(
+ {"realm": realm, "result": {"type": "number", "value": 3}}, result
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/serialization_options.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/serialization_options.py
new file mode 100644
index 0000000000..4084ec4820
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/serialization_options.py
@@ -0,0 +1,569 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, SerializationOptions
+
+from ... import any_string, recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "include_shadow_tree, shadow_root_mode, contains_children, expected",
+ [
+ (
+ None,
+ "open",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ None,
+ "closed",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "none",
+ "open",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "none",
+ "closed",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "open",
+ "open",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 11,
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "open",
+ },
+ },
+ ),
+ (
+ "open",
+ "closed",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "all",
+ "open",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 11,
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "open",
+ },
+ },
+ ),
+ (
+ "all",
+ "closed",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 11,
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "closed",
+ },
+ },
+ ),
+ ],
+ ids=[
+ "default mode for open shadow root",
+ "default mode for closed shadow root",
+ "'none' mode for open shadow root",
+ "'none' mode for closed shadow root",
+ "'open' mode for open shadow root",
+ "'open' mode for closed shadow root",
+ "'all' mode for open shadow root",
+ "'all' mode for closed shadow root",
+ ],
+)
+async def test_include_shadow_tree_for_custom_element(
+ bidi_session,
+ top_context,
+ get_test_page,
+ include_shadow_tree,
+ shadow_root_mode,
+ contains_children,
+ expected,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(shadow_root_mode=shadow_root_mode),
+ wait="complete",
+ )
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => document.querySelector("custom-element")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(
+ include_shadow_tree=include_shadow_tree, max_dom_depth=1
+ ),
+ )
+
+ recursive_compare(expected, result["value"]["shadowRoot"])
+
+ # Explicitely check for children because recursive_compare skips it
+ if not contains_children:
+ assert "children" not in result["value"]["shadowRoot"]["value"]
+
+
+@pytest.mark.parametrize(
+ "include_shadow_tree, contains_children, expected",
+ [
+ (
+ None,
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"childNodeCount": 1, "mode": "open", "nodeType": 11},
+ },
+ ),
+ (
+ "none",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"childNodeCount": 1, "mode": "open", "nodeType": 11},
+ },
+ ),
+ (
+ "open",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "nodeType": 11,
+ "mode": "open",
+ },
+ },
+ ),
+ (
+ "all",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "open",
+ "nodeType": 11,
+ },
+ },
+ ),
+ ],
+ ids=[
+ "default mode",
+ "'none' mode",
+ "'open' mode",
+ "'all' mode",
+ ],
+)
+async def test_include_shadow_tree_for_shadow_root(
+ bidi_session,
+ top_context,
+ get_test_page,
+ include_shadow_tree,
+ contains_children,
+ expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(),
+ wait="complete",
+ )
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => document.querySelector("custom-element").shadowRoot""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(
+ include_shadow_tree=include_shadow_tree, max_dom_depth=1
+ ),
+ )
+
+ recursive_compare(expected, result)
+
+ # Explicitely check for children because recursive_compare skips it
+ if not contains_children:
+ assert "children" not in result["value"]
+
+
+@pytest.mark.parametrize(
+ "max_dom_depth, expected",
+ [
+ (
+ None,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ (
+ 0,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ (
+ 1,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "children": [
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ (
+ 2,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "children": [
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 0,
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ ],
+)
+async def test_max_dom_depth(
+ bidi_session, top_context, get_test_page, max_dom_depth, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=get_test_page(), wait="complete"
+ )
+ result = await bidi_session.script.call_function(
+ function_declaration="""() => document.querySelector("div#with-children")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(max_dom_depth=max_dom_depth),
+ )
+
+ recursive_compare(expected, result)
+
+
+async def test_max_dom_depth_null(
+ bidi_session,
+ send_blocking_command,
+ top_context,
+ get_test_page,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=get_test_page(), wait="complete"
+ )
+ result = await send_blocking_command(
+ "script.callFunction",
+ {
+ "functionDeclaration": """() => document.querySelector("div#with-children")""",
+ "target": ContextTarget(top_context["context"]),
+ "awaitPromise": True,
+ "serializationOptions": {"maxDomDepth": None},
+ },
+ )
+
+ recursive_compare(
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 2,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 0,
+ "children": [],
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 0,
+ "children": [],
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ },
+ ],
+ "attributes": {"id": "with-children"},
+ "shadowRoot": None,
+ },
+ },
+ result["result"],
+ )
+
+
+@pytest.mark.parametrize(
+ "max_object_depth, expected",
+ [
+ (
+ None,
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "array", "value": [{"type": "number", "value": 2}]},
+ ],
+ },
+ ),
+ (0, {"type": "array"}),
+ (
+ 1,
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "array"},
+ ],
+ },
+ ),
+ (
+ 2,
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "array", "value": [{"type": "number", "value": 2}]},
+ ],
+ },
+ ),
+ ],
+)
+async def test_max_object_depth(bidi_session, top_context, max_object_depth, expected):
+ result = await bidi_session.script.call_function(
+ function_declaration="() => [1, [2]]",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(max_object_depth=max_object_depth),
+ )
+
+ assert result == expected
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/strict_mode.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/strict_mode.py
new file mode 100644
index 0000000000..9256713275
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/strict_mode.py
@@ -0,0 +1,40 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+from ... import recursive_compare
+from .. import specific_error_response
+
+
+@pytest.mark.asyncio
+async def test_strict_mode(bidi_session, top_context):
+
+ # As long as there is no `SOME_VARIABLE`, the command should fail in strict mode.
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.call_function(
+ function_declaration="()=>{'use strict';return SOME_VARIABLE=1}",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ recursive_compare(specific_error_response({"type": "error"}), exception.value.result)
+
+ # In non-strict mode, the command should succeed and global `SOME_VARIABLE` should be created.
+ result = await bidi_session.script.call_function(
+ function_declaration="()=>{return SOME_VARIABLE=1}",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ assert result == {
+ "type": "number",
+ "value": 1,
+ }
+
+ # Access created by the previous command `SOME_VARIABLE`.
+ result = await bidi_session.script.call_function(
+ function_declaration="()=>{'use strict';return SOME_VARIABLE=1}",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ assert result == {
+ "type": "number",
+ "value": 1,
+ }
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/target.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/target.py
new file mode 100644
index 0000000000..d6550d67d5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/target.py
@@ -0,0 +1,33 @@
+import pytest
+
+from webdriver.bidi.modules.script import (
+ ContextTarget,
+)
+
+from ... import recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_target_context_and_realm(bidi_session, top_context, new_tab):
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => { window.foo = 3; }",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+ realm = result["realm"]
+
+ # Make sure that realm argument is ignored and
+ # script is executed in the right context.
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="() => window.foo",
+ target={"context": new_tab["context"], "realm": realm},
+ await_promise=True,
+ )
+
+ assert realm != result["realm"]
+ recursive_compare(
+ {"realm": result["realm"], "result": {"type": "undefined"}}, result
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/this.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/this.py
new file mode 100644
index 0000000000..2893bb037a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/this.py
@@ -0,0 +1,149 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.asyncio
+async def test_this(bidi_session, top_context):
+ result = await bidi_session.script.call_function(
+ function_declaration="function(){return this.some_property}",
+ this={
+ "type": "object",
+ "value": [[
+ "some_property",
+ {
+ "type": "number",
+ "value": 42,
+ }]]},
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+ assert result == {
+ 'type': 'number',
+ 'value': 42,
+ }
+
+
+@pytest.mark.asyncio
+async def test_default_this(bidi_session, top_context):
+ result = await bidi_session.script.call_function(
+ function_declaration="function(){return this}",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]))
+
+ recursive_compare({
+ "type": 'window',
+ }, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "value_fn, function_declaration",
+ [
+ (
+ lambda value: value,
+ "function() { return this === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "object", "value": [["nested", value]]}),
+ "function() { return this.nested === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "array", "value": [value]}),
+ "function() { return this[0] === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "map", "value": [["foobar", value]]}),
+ "function() { return this.get('foobar') === window.SOME_OBJECT; }",
+ ),
+ (
+ lambda value: ({"type": "set", "value": [value]}),
+ "function() { return this.has(window.SOME_OBJECT); }",
+ ),
+ ],
+)
+async def test_remote_value_deserialization(
+ bidi_session, top_context, call_function, evaluate, value_fn, function_declaration
+):
+ remote_value = await evaluate(
+ "window.SOME_OBJECT = {SOME_PROPERTY:'SOME_VALUE'}; window.SOME_OBJECT",
+ result_ownership="root",
+ )
+
+ # Check that a remote value can be successfully deserialized as the "this"
+ # parameter and compared against the original object in the page.
+ result = await call_function(
+ function_declaration=function_declaration,
+ this=value_fn(remote_value),
+ )
+ assert result == {"type": "boolean", "value": True}
+
+ # Reload the page to cleanup the state
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=top_context["url"], wait="complete"
+ )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "channel, expected_data",
+ [
+ (
+ {"type": "channel", "value": {"channel": "channel_name"}},
+ {"type": "object", "value": [["foo", {"type": "string", "value": "bar"}]]},
+ ),
+ (
+ {
+ "type": "channel",
+ "value": {
+ "channel": "channel_name",
+ "serializationOptions": {
+ "maxObjectDepth": 0,
+ },
+ },
+ },
+ {"type": "object"},
+ ),
+ (
+ {
+ "type": "channel",
+ "value": {"channel": "channel_name", "ownership": "root"},
+ },
+ {
+ "handle": any_string,
+ "type": "object",
+ "value": [["foo", {"type": "string", "value": "bar"}]],
+ },
+ ),
+ ],
+ ids=["default", "with serializationOptions", "with ownership"],
+)
+async def test_channel(
+ bidi_session, top_context, subscribe_events, wait_for_event,
+ wait_for_future_safe, channel, expected_data
+):
+ await subscribe_events(["script.message"])
+
+ on_entry_added = wait_for_event("script.message")
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="function() { return this({'foo': 'bar'}) }",
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ this=channel,
+ )
+ event_data = await wait_for_future_safe(on_entry_added)
+
+ recursive_compare(
+ {
+ "channel": "channel_name",
+ "data": expected_data,
+ "source": {
+ "realm": result["realm"],
+ "context": top_context["context"],
+ },
+ },
+ event_data,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/user_activation.py b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/user_activation.py
new file mode 100644
index 0000000000..3c1b039981
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/call_function/user_activation.py
@@ -0,0 +1,42 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("user_activation", [True, False])
+async def test_userActivation(bidi_session, top_context, user_activation):
+ # Consume any previously set activation.
+ await bidi_session.script.evaluate(expression="""window.open();""",
+ target=ContextTarget(
+ top_context["context"]),
+ await_promise=False)
+
+ result = await bidi_session.script.call_function(
+ function_declaration=
+ "() => navigator.userActivation.isActive && navigator.userActivation.hasBeenActive",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ user_activation=user_activation)
+
+ assert result == {"type": "boolean", "value": user_activation}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("user_activation", [True, False])
+async def test_userActivation_copy(bidi_session, top_context, user_activation):
+ # Consume any previously set activation.
+ await bidi_session.script.evaluate(expression="""window.open();""",
+ target=ContextTarget(
+ top_context["context"]),
+ await_promise=False)
+
+ result = await bidi_session.script.call_function(
+ function_declaration=
+ "() => document.body.appendChild(document.createTextNode('test')) && " +
+ "document.execCommand('selectAll') && document.execCommand('copy')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ user_activation=user_activation)
+
+ assert result == {"type": "boolean", "value": user_activation}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/node_shared_id.py b/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/node_shared_id.py
new file mode 100644
index 0000000000..aeb2bc4597
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/node_shared_id.py
@@ -0,0 +1,101 @@
+import pytest
+
+from webdriver import ShadowRoot, WebElement
+from webdriver.bidi.modules.script import ContextTarget
+
+pytestmark = pytest.mark.asyncio
+
+DOCUMENT_FRAGMENT_NODE = 11
+ELEMENT_NODE = 1
+
+
+async def test_web_element_reference_created_in_classic(
+ bidi_session,
+ current_session,
+ get_test_page,
+ top_context,
+):
+ current_session.url = get_test_page()
+
+ node = current_session.execute_script(
+ """return document.querySelector("div#with-children")"""
+ )
+ shared_id = node.id
+
+ # Use element reference from WebDriver classic in WebDriver BiDi
+ result = await bidi_session.script.call_function(
+ function_declaration="(node)=>{return node.nodeType}",
+ arguments=[{"sharedId": shared_id}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {"type": "number", "value": ELEMENT_NODE}
+
+
+async def test_web_element_reference_created_in_bidi(
+ bidi_session,
+ current_session,
+ get_test_page,
+ top_context,
+):
+ current_session.url = get_test_page()
+
+ result = await bidi_session.script.evaluate(
+ expression="""document.querySelector("div#with-children")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ nodeType = result["value"]["nodeType"]
+ assert nodeType == ELEMENT_NODE
+
+ # Use element reference from WebDriver BiDi in WebDriver classic
+ node = WebElement(current_session, result["sharedId"])
+ nodeType = current_session.execute_script(
+ """return arguments[0].nodeType""", args=(node,)
+ )
+ assert nodeType == ELEMENT_NODE
+
+
+@pytest.mark.parametrize("shadow_root_mode", ["open", "closed"])
+async def test_shadow_root_reference_created_in_classic(
+ bidi_session, current_session, get_test_page, top_context, shadow_root_mode
+):
+ current_session.url = get_test_page(shadow_root_mode=shadow_root_mode)
+
+ node = current_session.execute_script(
+ """return document.querySelector("custom-element")"""
+ )
+ shared_id = node.shadow_root.id
+
+ # Use shadow root reference from WebDriver classic in WebDriver BiDi
+ result = await bidi_session.script.call_function(
+ function_declaration="(node)=>{return node.nodeType}",
+ arguments=[{"sharedId": shared_id}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {"type": "number", "value": DOCUMENT_FRAGMENT_NODE}
+
+
+@pytest.mark.parametrize("shadow_root_mode", ["open", "closed"])
+async def test_shadow_root_reference_created_in_bidi(
+ bidi_session, current_session, get_test_page, top_context, shadow_root_mode
+):
+ current_session.url = get_test_page(shadow_root_mode=shadow_root_mode)
+
+ result = await bidi_session.script.evaluate(
+ expression="""document.querySelector("custom-element")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ shared_id_for_shadow_root = result["value"]["shadowRoot"]["sharedId"]
+
+ # Use shadow root reference from WebDriver BiDi in WebDriver classic
+ node = ShadowRoot(current_session, shared_id_for_shadow_root)
+ nodeType = current_session.execute_script(
+ """return arguments[0].nodeType""", args=(node,)
+ )
+ assert nodeType == DOCUMENT_FRAGMENT_NODE
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/window_reference.py b/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/window_reference.py
new file mode 100644
index 0000000000..1588303be0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/classic_interop/window_reference.py
@@ -0,0 +1,124 @@
+import pytest
+
+from webdriver import WebFrame, WebWindow
+from webdriver.bidi.modules.script import ContextTarget
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_web_window_reference_created_in_classic(
+ bidi_session,
+ current_session,
+ get_test_page,
+):
+ handle = current_session.new_window(type_hint="tab")
+ current_session.window_handle = handle
+ current_session.url = get_test_page()
+
+ expected_test_value = "bar"
+ window = current_session.execute_script(
+ f"window.foo = '{expected_test_value}'; return window;"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree()
+ assert len(contexts) == 2
+
+ assert window.id == contexts[1]["context"]
+
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(window.id),
+ await_promise=False,
+ )
+
+ assert result["value"] == expected_test_value
+
+
+async def test_web_frame_reference_created_in_classic(
+ bidi_session,
+ current_session,
+ get_test_page,
+):
+ handle = current_session.new_window(type_hint="tab")
+ current_session.window_handle = handle
+ current_session.url = get_test_page()
+
+ expected_test_value = "foo"
+ frame = current_session.execute_script(
+ f"window.frames[0].bar='{expected_test_value}'; return window.frames[0]"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree()
+ assert len(contexts) == 2
+
+ assert frame.id == contexts[1]["children"][0]["context"]
+
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(frame.id),
+ await_promise=False,
+ )
+
+ assert result["value"] == expected_test_value
+
+
+async def test_web_window_reference_created_in_bidi(
+ bidi_session,
+ current_session,
+ get_test_page,
+ new_tab
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=get_test_page(),
+ wait="complete"
+ )
+
+ expected_test_value = "bar"
+ result = await bidi_session.script.evaluate(
+ expression=f"window.xyz = '{expected_test_value}'; window;",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ context_id = result["value"]["context"]
+
+ # Use window reference from WebDriver BiDi in WebDriver classic
+ current_session.window_handle = new_tab["context"]
+ window = WebWindow(current_session, context_id)
+ test_value = current_session.execute_script(
+ """return arguments[0].xyz""", args=(window,)
+ )
+
+ assert test_value == expected_test_value
+
+
+async def test_web_frame_reference_created_in_bidi(
+ bidi_session,
+ current_session,
+ get_test_page,
+ new_tab
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=get_test_page(),
+ wait="complete"
+ )
+
+ expected_test_value = "foo"
+ result = await bidi_session.script.evaluate(
+ expression=f"window.frames[0].baz='{expected_test_value}'; window.frames[0];",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ context_id = result["value"]["context"]
+
+ # Use window reference from WebDriver BiDi in WebDriver classic
+ current_session.window_handle = new_tab["context"]
+ window = WebFrame(current_session, context_id)
+ test_value = current_session.execute_script(
+ """return arguments[0].baz""", args=(window,)
+ )
+
+ assert test_value == expected_test_value
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/script/conftest.py
new file mode 100644
index 0000000000..c3d39fcb9f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/conftest.py
@@ -0,0 +1,67 @@
+import pytest
+import pytest_asyncio
+from typing import Any, List, Mapping, Optional
+
+from webdriver.bidi.modules.script import ContextTarget, OwnershipModel, SerializationOptions
+
+
+@pytest.fixture
+def call_function(bidi_session, top_context):
+ async def call_function(
+ function_declaration: str,
+ arguments: List[Mapping[str, Any]] = [],
+ this: Any = None,
+ context: str = top_context["context"],
+ sandbox: str = None,
+ result_ownership: OwnershipModel = OwnershipModel.NONE.value,
+ serialization_options: Optional[SerializationOptions] = None,
+ ) -> Mapping[str, Any]:
+ if sandbox is None:
+ target = ContextTarget(context)
+ else:
+ target = ContextTarget(context, sandbox)
+
+ result = await bidi_session.script.call_function(
+ function_declaration=function_declaration,
+ arguments=arguments,
+ this=this,
+ await_promise=False,
+ result_ownership=result_ownership,
+ serialization_options=serialization_options,
+ target=target,
+ )
+ return result
+
+ return call_function
+
+
+@pytest_asyncio.fixture
+async def default_realm(bidi_session, top_context):
+ realms = await bidi_session.script.get_realms(context=top_context["context"])
+ return realms[0]["realm"]
+
+
+@pytest.fixture
+def evaluate(bidi_session, top_context):
+ async def evaluate(
+ expression: str,
+ context: str = top_context["context"],
+ sandbox: str = None,
+ result_ownership: OwnershipModel = OwnershipModel.NONE.value,
+ serialization_options: Optional[SerializationOptions] = None,
+ ) -> Mapping[str, Any]:
+ if sandbox is None:
+ target = ContextTarget(context)
+ else:
+ target = ContextTarget(context, sandbox)
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ await_promise=False,
+ result_ownership=result_ownership,
+ serialization_options=serialization_options,
+ target=target,
+ )
+ return result
+
+ return evaluate
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/disown/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/disown/handles.py b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/handles.py
new file mode 100644
index 0000000000..1dfa318cd1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/handles.py
@@ -0,0 +1,173 @@
+import pytest
+
+import webdriver.bidi.error as error
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import assert_handle
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_basic_handle(bidi_session, top_context, call_function):
+ remote_value = await bidi_session.script.evaluate(
+ expression="({a:1})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert_handle(remote_value, True)
+
+ result = await call_function("arg => arg.a", [remote_value])
+
+ assert result == {"type": "number", "value": 1}
+
+ await bidi_session.script.disown(
+ handles=[remote_value["handle"]], target=ContextTarget(top_context["context"])
+ )
+
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value])
+
+
+async def test_multiple_handles_for_different_objects(
+ bidi_session, top_context, call_function
+):
+ # Create a handle
+ remote_value_a = await bidi_session.script.evaluate(
+ expression="({a:1})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ remote_value_b = await bidi_session.script.evaluate(
+ expression="({b:2})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ remote_value_c = await bidi_session.script.evaluate(
+ expression="({c:3})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert_handle(remote_value_a, True)
+ assert_handle(remote_value_b, True)
+ assert_handle(remote_value_c, True)
+
+ # disown a and b
+ await bidi_session.script.disown(
+ handles=[remote_value_a["handle"], remote_value_b["handle"]],
+ target=ContextTarget(top_context["context"]),
+ )
+
+ # using handle a or b should raise an exception
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value_a])
+
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.b", [remote_value_b])
+
+ # remote value c should still work
+ result = await call_function("arg => arg.c", [remote_value_c])
+
+ assert result == {"type": "number", "value": 3}
+
+ # disown c
+ await bidi_session.script.disown(
+ handles=[remote_value_c["handle"]], target=ContextTarget(top_context["context"])
+ )
+
+ # using handle c should raise an exception
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.c", [remote_value_c])
+
+
+async def test_multiple_handles_for_same_object(
+ bidi_session, top_context, call_function
+):
+ remote_value1 = await bidi_session.script.evaluate(
+ expression="window.test = { a: 1 }; window.test",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+ assert_handle(remote_value1, True)
+
+ remote_value2 = await bidi_session.script.evaluate(
+ expression="window.test",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+ assert_handle(remote_value2, True)
+
+ # Check that both handles can be used
+ result = await call_function("arg => arg.a", [remote_value1])
+ assert result == {"type": "number", "value": 1}
+
+ result = await call_function("arg => arg.a", [remote_value2])
+ assert result == {"type": "number", "value": 1}
+
+ # Check that both handles point to the same value
+ result = await call_function(
+ "(arg1, arg2) => arg1 === arg2", [remote_value1, remote_value2]
+ )
+ assert result == {"type": "boolean", "value": True}
+
+ # Disown the handle 1
+ await bidi_session.script.disown(
+ handles=[remote_value1["handle"]], target=ContextTarget(top_context["context"])
+ )
+
+ # Using handle 1 should raise an exception
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value1])
+
+ # Using handle 2 should still work
+ result = await call_function("arg => arg.a", [remote_value2])
+ assert result == {"type": "number", "value": 1}
+
+ # Disown the handle 2
+ await bidi_session.script.disown(
+ handles=[remote_value2["handle"]], target=ContextTarget(top_context["context"])
+ )
+
+ # Using handle 2 should raise an exception
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value2])
+
+
+async def test_unknown_handle(bidi_session, top_context, call_function):
+ # Create a handle
+ remote_value = await bidi_session.script.evaluate(
+ expression="({a:1})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert_handle(remote_value, True)
+
+ # An unknown handle should not remove other handles, and should not fail
+ await bidi_session.script.disown(
+ handles=["unknown_handle"], target=ContextTarget(top_context["context"])
+ )
+
+ result = await call_function("arg => arg.a", [remote_value])
+
+ assert result == {"type": "number", "value": 1}
+
+ # Passing an unknown handle with an existing handle should disown the existing one
+ await bidi_session.script.disown(
+ handles=["unknown_handle", remote_value["handle"]],
+ target=ContextTarget(top_context["context"]),
+ )
+
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/disown/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/invalid.py
new file mode 100644
index 0000000000..f9849f3e39
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/invalid.py
@@ -0,0 +1,68 @@
+import pytest
+import webdriver.bidi.error as error
+
+from webdriver.bidi.modules.script import ContextTarget, RealmTarget
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("target", [None, False, "foo", 42, {}, []])
+async def test_params_target_invalid_type(bidi_session, target):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.disown(
+ handles=[],
+ target=target)
+
+
+@pytest.mark.parametrize("context", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.disown(
+ handles=[],
+ target=ContextTarget(context))
+
+
+@pytest.mark.parametrize("sandbox", [False, 42, {}, []])
+async def test_params_sandbox_invalid_type(bidi_session, top_context, sandbox):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.disown(
+ handles=[],
+ target=ContextTarget(top_context["context"], sandbox))
+
+
+async def test_params_context_unknown(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.disown(
+ handles=[],
+ target=ContextTarget("_UNKNOWN_"))
+
+
+@pytest.mark.parametrize("realm", [None, False, 42, {}, []])
+async def test_params_realm_invalid_type(bidi_session, realm):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.disown(
+ handles=[],
+ target=RealmTarget(realm))
+
+
+async def test_params_realm_unknown(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.disown(
+ handles=[],
+ target=RealmTarget("_UNKNOWN_"))
+
+
+@pytest.mark.parametrize("handles", [None, False, "foo", 42, {}])
+async def test_params_handles_invalid_type(bidi_session, top_context, handles):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.disown(
+ handles=handles,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("handle", [None, False, 42, {}, []])
+async def test_params_handles_invalid_handle_type(bidi_session, top_context, handle):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.disown(
+ handles=[handle],
+ target=ContextTarget(top_context["context"]))
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/disown/target.py b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/target.py
new file mode 100644
index 0000000000..f01dcb3b71
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/disown/target.py
@@ -0,0 +1,136 @@
+import pytest
+
+import webdriver.bidi.error as error
+
+from webdriver.bidi.modules.script import ContextTarget, RealmTarget
+
+from ... import assert_handle
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_realm(bidi_session, top_context, call_function):
+ remote_value = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="({a:1})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert_handle(remote_value["result"], True)
+
+ result = await call_function("arg => arg.a", [remote_value["result"]])
+
+ assert result == {"type": "number", "value": 1}
+
+ await bidi_session.script.disown(
+ handles=[remote_value["result"]["handle"]],
+ target=RealmTarget(remote_value["realm"]),
+ )
+
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value["result"]])
+
+
+async def test_sandbox(bidi_session, top_context, call_function):
+ # Create a remote value outside of any sandbox
+ remote_value = await bidi_session.script.evaluate(
+ expression="({a:'without sandbox'})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"]),
+ )
+
+ # Create a remote value from a sandbox
+ sandbox_value = await bidi_session.script.evaluate(
+ expression="({a:'with sandbox'})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"], "basic_sandbox"),
+ )
+
+ # Try to disown the non-sandboxed remote value from the sandbox
+ await bidi_session.script.disown(
+ handles=[remote_value["handle"]],
+ target=ContextTarget(top_context["context"], "basic_sandbox"),
+ )
+
+ # Check that the remote value is still working
+ result = await call_function("arg => arg.a", [remote_value])
+ assert result == {"type": "string", "value": "without sandbox"}
+
+ # Try to disown the sandbox value:
+ # - from the non-sandboxed top context
+ # - from another sandbox
+ await bidi_session.script.disown(
+ handles=[sandbox_value["handle"]], target=ContextTarget(top_context["context"])
+ )
+ await bidi_session.script.disown(
+ handles=[sandbox_value["handle"]],
+ target=ContextTarget(top_context["context"], "another_sandbox"),
+ )
+
+ # Check that the sandbox remote value is still working
+ result = await call_function(
+ "arg => arg.a", [sandbox_value], sandbox="basic_sandbox"
+ )
+ assert result == {"type": "string", "value": "with sandbox"}
+
+ # Disown the sandbox remote value from the correct sandbox
+ await bidi_session.script.disown(
+ handles=[sandbox_value["handle"]],
+ target=ContextTarget(top_context["context"], "basic_sandbox"),
+ )
+
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [sandbox_value], sandbox="basic_sandbox")
+
+ # Disown the non-sandboxed remote value from the top context
+ await bidi_session.script.disown(
+ handles=[remote_value["handle"]], target=ContextTarget(top_context["context"])
+ )
+
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value], sandbox="basic_sandbox")
+
+
+async def test_context_and_realm(bidi_session, top_context, new_tab, call_function):
+ # Create a remote value outside of any sandbox.
+ result_in_default_realm = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="({a:'without sandbox'})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(new_tab["context"]),
+ )
+ remote_value = result_in_default_realm["result"]
+
+ # Create a remote value from a sandbox.
+ result_in_sandbox = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="({a:'with sandbox'})",
+ await_promise=False,
+ result_ownership="root",
+ target=ContextTarget(top_context["context"], "basic_sandbox"),
+ )
+ sandbox_value = result_in_sandbox["result"]
+
+ # Make sure that realm argument is ignored and the value is disowned
+ # in the default realm of another context.
+ await bidi_session.script.disown(
+ handles=[remote_value["handle"]],
+ target={
+ "context": new_tab["context"],
+ "realm": result_in_default_realm["realm"]
+ }
+ )
+
+ with pytest.raises(error.NoSuchHandleException):
+ await call_function("arg => arg.a", [remote_value], None, new_tab["context"])
+
+ # Check that the sandbox remote value is still working.
+ result = await call_function(
+ "arg => arg.a", [sandbox_value], sandbox="basic_sandbox"
+ )
+ assert result == {"type": "string", "value": "with sandbox"}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/await_promise.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/await_promise.py
new file mode 100644
index 0000000000..fd330847e1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/await_promise.py
@@ -0,0 +1,202 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+
+from ... import any_int, any_string, recursive_compare
+from .. import any_stack_trace, PRIMITIVE_VALUES
+
+
+@pytest.mark.asyncio
+async def test_await_promise_delayed(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="""
+ new Promise(r => {{
+ setTimeout(() => r("SOME_DELAYED_RESULT"), 0);
+ }})
+ """,
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {"type": "string", "value": "SOME_DELAYED_RESULT"}
+
+
+@pytest.mark.asyncio
+async def test_await_promise_rejected(bidi_session, top_context):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression="Promise.reject('SOME_REJECTED_RESULT')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": {"type": "string", "value": "SOME_REJECTED_RESULT"},
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
+
+
+@pytest.mark.asyncio
+async def test_await_promise_resolved(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="Promise.resolve('SOME_RESOLVED_RESULT')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+
+ assert result == {"type": "string", "value": "SOME_RESOLVED_RESULT"}
+
+
+@pytest.mark.asyncio
+async def test_await_resolve_array(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="Promise.resolve([1, 'text', true, ['will be serialized']])",
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "string", "value": "text"},
+ {"type": "boolean", "value": True},
+ {"type": "array", "value": [{"type": "string", "value": "will be serialized"}]},
+ ],
+ }
+
+
+@pytest.mark.asyncio
+async def test_await_resolve_date(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="Promise.resolve(new Date(0))",
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {
+ "type": "date",
+ "value": "1970-01-01T00:00:00.000Z",
+ }
+
+
+@pytest.mark.asyncio
+async def test_await_resolve_map(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="""
+ Promise.resolve(
+ new Map([
+ ['key1', 'value1'],
+ [2, new Date(0)],
+ ['key3', new Map([['key4', 'serialized']])]
+ ])
+ )""",
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {
+ "type": "map",
+ "value": [
+ ["key1", {"type": "string", "value": "value1"}],
+ [
+ {"type": "number", "value": 2},
+ {"type": "date", "value": "1970-01-01T00:00:00.000Z"},
+ ],
+ ["key3", {"type": "map", "value": [[
+ "key4",
+ {"type": "string", "value": "serialized"}
+ ]]}],
+ ],
+ }
+
+
+@pytest.mark.parametrize("expression, expected", PRIMITIVE_VALUES)
+@pytest.mark.asyncio
+async def test_await_resolve_primitive(
+ bidi_session, top_context, expression, expected
+):
+ result = await bidi_session.script.evaluate(
+ expression=f"Promise.resolve({expression})",
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == expected
+
+
+@pytest.mark.asyncio
+async def test_await_resolve_regexp(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="Promise.resolve(/test/i)",
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {
+ "type": "regexp",
+ "value": {
+ "pattern": "test",
+ "flags": "i",
+ },
+ }
+
+
+@pytest.mark.asyncio
+async def test_await_resolve_set(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="""
+ Promise.resolve(
+ new Set([
+ 'value1',
+ 2,
+ true,
+ new Date(0),
+ new Set([-1, 'serialized'])
+ ])
+ )""",
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert result == {
+ "type": "set",
+ "value": [
+ {"type": "string", "value": "value1"},
+ {"type": "number", "value": 2},
+ {"type": "boolean", "value": True},
+ {"type": "date", "value": "1970-01-01T00:00:00.000Z"},
+ {"type": "set", "value": [{"type": "number", "value": -1}, {"type": "string", "value": "serialized"}]},
+ ],
+ }
+
+
+@pytest.mark.asyncio
+async def test_no_await_promise_rejected(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="Promise.reject('SOME_REJECTED_RESULT')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare({"type": "promise"}, result)
+
+
+@pytest.mark.asyncio
+async def test_no_await_promise_resolved(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="Promise.resolve('SOME_RESOLVED_RESULT')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare({"type": "promise"}, result)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/evaluate.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/evaluate.py
new file mode 100644
index 0000000000..34889877c2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/evaluate.py
@@ -0,0 +1,95 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, RealmTarget
+
+from ... import recursive_compare
+
+
+@pytest.mark.asyncio
+async def test_evaluate(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+ assert result == {
+ "type": "number",
+ "value": 3}
+
+
+@pytest.mark.asyncio
+async def test_interact_with_dom(bidi_session, top_context):
+ result = await bidi_session.script.evaluate(
+ expression="'window.location.href: ' + window.location.href",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+ assert result == {
+ "type": "string",
+ "value": "window.location.href: about:blank"}
+
+
+@pytest.mark.asyncio
+async def test_target_realm(bidi_session, default_realm):
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo = 3",
+ target=RealmTarget(default_realm),
+ await_promise=True,
+ )
+
+ recursive_compare({"realm": default_realm, "result": {"type": "number", "value": 3}}, result)
+
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo",
+ target=RealmTarget(default_realm),
+ await_promise=True,
+ )
+
+ recursive_compare(
+ {"realm": default_realm, "result": {"type": "number", "value": 3}}, result
+ )
+
+
+@pytest.mark.asyncio
+async def test_different_target_realm(bidi_session):
+ await bidi_session.browsing_context.create(type_hint="tab")
+
+ realms = await bidi_session.script.get_realms()
+ first_tab_default_realm = realms[0]["realm"]
+ second_tab_default_realm = realms[1]["realm"]
+
+ assert first_tab_default_realm != second_tab_default_realm
+
+ await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo = 3",
+ target=RealmTarget(first_tab_default_realm),
+ await_promise=True,
+ )
+ await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo = 5",
+ target=RealmTarget(second_tab_default_realm),
+ await_promise=True,
+ )
+
+ top_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo",
+ target=RealmTarget(first_tab_default_realm),
+ await_promise=True,
+ )
+ recursive_compare(
+ {"realm": first_tab_default_realm, "result": {"type": "number", "value": 3}}, top_context_result
+ )
+
+ new_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo",
+ target=RealmTarget(second_tab_default_realm),
+ await_promise=True,
+ )
+ recursive_compare(
+ {"realm": second_tab_default_realm, "result": {"type": "number", "value": 5}}, new_context_result
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details.py
new file mode 100644
index 0000000000..f01d23e7bc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details.py
@@ -0,0 +1,84 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+
+from ... import any_int, any_string, recursive_compare
+from .. import any_stack_trace, PRIMITIVE_VALUES, REMOTE_VALUES
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expression, expected", PRIMITIVE_VALUES + REMOTE_VALUES)
+async def test_exception_details(bidi_session, top_context, expression,
+ expected):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression=f"throw {expression}",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": expected,
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
+
+
+@pytest.mark.asyncio
+async def test_invalid_script(bidi_session, top_context):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression="))) !!@@## some invalid JS script (((",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": {"type": "error"},
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("chained", [True, False])
+async def test_rejected_promise(bidi_session, top_context, chained):
+ if chained:
+ expression = "Promise.reject('error').then(() => { })"
+ else:
+ expression = "Promise.reject('error')"
+
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression=expression,
+ await_promise=True,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": {"type": "string", "value": "error"},
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details_await_promise.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details_await_promise.py
new file mode 100644
index 0000000000..2a88f83db2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/exception_details_await_promise.py
@@ -0,0 +1,32 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+
+from ... import any_int, any_string, recursive_compare
+from .. import any_stack_trace, PRIMITIVE_VALUES, REMOTE_VALUES
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expression, expected", PRIMITIVE_VALUES + REMOTE_VALUES)
+async def test_exception_details_await_promise(
+ bidi_session, top_context, expression, expected
+):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression=f"Promise.reject({expression})",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": expected,
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/internal_id.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/internal_id.py
new file mode 100644
index 0000000000..98742ef102
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/internal_id.py
@@ -0,0 +1,65 @@
+import pytest
+
+from ... import recursive_compare, any_string
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "return_structure, result_type",
+ [
+ ("[data, data]", "array"),
+ ("new Map([['foo', data],['bar', data]])", "map"),
+ ("({ 'foo': data, 'bar': data })", "object"),
+ ],
+)
+@pytest.mark.parametrize(
+ "expression, type",
+ [
+ ("[1]", "array"),
+ ("new Map([[true, false]])", "map"),
+ ("new Set(['baz'])", "set"),
+ ("{ baz: 'qux' }", "object"),
+ ],
+)
+async def test_remote_values_with_internal_id(
+ evaluate, return_structure, result_type, expression, type
+):
+ result = await evaluate(f"{{const data = {expression}; {return_structure}}}")
+ result_value = result["value"]
+
+ assert len(result_value) == 2
+
+ if result_type == "array":
+ value = [
+ {"type": type, "internalId": any_string},
+ {"type": type, "internalId": any_string},
+ ]
+ internalId1 = result_value[0]["internalId"]
+ internalId2 = result_value[1]["internalId"]
+ else:
+ value = [
+ ["foo", {"type": type, "internalId": any_string}],
+ ["bar", {"type": type, "internalId": any_string}],
+ ]
+ internalId1 = result_value[0][1]["internalId"]
+ internalId2 = result_value[1][1]["internalId"]
+
+ # Make sure that the same duplicated objects have the same internal ids
+ assert internalId1 == internalId2
+
+ recursive_compare(value, result_value)
+
+
+@pytest.mark.asyncio
+async def test_different_remote_values_have_unique_internal_ids(evaluate):
+ result = await evaluate(
+ "{const obj1 = [1]; const obj2 = {'foo': 'bar'}; [obj1, obj2, obj1, obj2]}"
+ )
+
+ assert len(result["value"]) == 4
+
+ internalId1 = result["value"][0]["internalId"]
+ internalId2 = result["value"][1]["internalId"]
+
+ # Make sure that different duplicated objects have different internal ids
+ assert internalId1 != internalId2
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/invalid.py
new file mode 100644
index 0000000000..f27720a204
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/invalid.py
@@ -0,0 +1,164 @@
+import pytest
+import webdriver.bidi.error as error
+
+from webdriver.bidi.modules.script import ContextTarget, RealmTarget, SerializationOptions
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("target", [None, False, "foo", 42, {}, []])
+async def test_params_target_invalid_type(bidi_session, target):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ target=target,
+ await_promise=True)
+
+
+@pytest.mark.parametrize("context", [None, False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ target=ContextTarget(context),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("sandbox", [False, 42, {}, []])
+async def test_params_sandbox_invalid_type(bidi_session, top_context, sandbox):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"], sandbox),
+ await_promise=True)
+
+
+async def test_params_context_unknown(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ target=ContextTarget("_UNKNOWN_"),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("realm", [None, False, 42, {}, []])
+async def test_params_realm_invalid_type(bidi_session, realm):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ target=RealmTarget(realm),
+ await_promise=True)
+
+
+async def test_params_realm_unknown(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ target=RealmTarget("_UNKNOWN_"),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("expression", [None, False, 42, {}, []])
+async def test_params_expression_invalid_type(bidi_session, top_context, expression):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("await_promise", [None, "False", 0, 42, {}, []])
+async def test_params_await_promise_invalid_type(bidi_session, top_context, await_promise):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ await_promise=await_promise,
+ target=ContextTarget(top_context["context"]))
+
+
+@pytest.mark.parametrize("result_ownership", [False, "_UNKNOWN_", 42, {}, []])
+async def test_params_result_ownership_invalid_value(bidi_session, top_context, result_ownership):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("serialization_options", [False, "_UNKNOWN_", 42, []])
+async def test_params_serialization_options_invalid_type(bidi_session, top_context, serialization_options):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ serialization_options=serialization_options,
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("max_dom_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_max_dom_depth_invalid_type(bidi_session, top_context, max_dom_depth):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ serialization_options=SerializationOptions(max_dom_depth=max_dom_depth),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+async def test_params_max_dom_depth_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ serialization_options=SerializationOptions(max_dom_depth=-1),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("max_object_depth", [False, "_UNKNOWN_", {}, []])
+async def test_params_max_object_depth_invalid_type(bidi_session, top_context, max_object_depth):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ serialization_options=SerializationOptions(max_object_depth=max_object_depth),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+async def test_params_max_object_depth_invalid_value(bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ serialization_options=SerializationOptions(max_object_depth=-1),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("include_shadow_tree", [False, 42, {}, []])
+async def test_params_include_shadow_tree_invalid_type(bidi_session, top_context, include_shadow_tree):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ serialization_options=SerializationOptions(include_shadow_tree=include_shadow_tree),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+async def test_params_include_shadow_tree_invalid_value(
+ bidi_session, top_context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ serialization_options=SerializationOptions(include_shadow_tree="foo"),
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+
+
+@pytest.mark.parametrize("user_activation", ["foo", 42, {}, []])
+async def test_params_user_activation_invalid_type(bidi_session, top_context, user_activation):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.evaluate(
+ expression="1 + 2",
+ user_activation=user_activation,
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/primitive_values.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/primitive_values.py
new file mode 100644
index 0000000000..6ca053c036
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/primitive_values.py
@@ -0,0 +1,16 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget
+from .. import PRIMITIVE_VALUES
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expression, expected", PRIMITIVE_VALUES)
+async def test_primitive_values(bidi_session, top_context, expression,
+ expected):
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+
+ assert result == expected
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/remote_values.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/remote_values.py
new file mode 100644
index 0000000000..c3f29cbab5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/remote_values.py
@@ -0,0 +1,145 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, SerializationOptions
+from ... import recursive_compare
+from .. import REMOTE_VALUES
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("expression, expected", REMOTE_VALUES)
+async def test_remote_values(bidi_session, top_context, expression, expected):
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_object_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_window_context_top_level(bidi_session, top_context, await_promise):
+ result = await bidi_session.script.evaluate(
+ expression="window",
+ target=ContextTarget(top_context["context"]),
+ await_promise=await_promise,
+ serialization_options=SerializationOptions(max_object_depth=1),
+ )
+
+ recursive_compare(
+ {
+ "type": "window",
+ "value": {
+ "context": top_context["context"]
+ }
+ }, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("domain", ["", "alt"],
+ ids=["same_origin", "cross_origin"])
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_window_context_iframe_window(
+ bidi_session, top_context, inline, domain, await_promise):
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ all_contexts = await bidi_session.browsing_context.get_tree()
+ iframe_context = all_contexts[0]["children"][0]
+
+ result = await bidi_session.script.evaluate(
+ expression="window",
+ target=ContextTarget(iframe_context["context"]),
+ await_promise=await_promise,
+ serialization_options=SerializationOptions(max_object_depth=1),
+ )
+
+ recursive_compare(
+ {
+ "type": "window",
+ "value": {
+ "context": iframe_context["context"]
+ }
+ }, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("domain", ["", "alt"],
+ ids=["same_origin", "cross_origin"])
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_window_context_iframe_content_window(
+ bidi_session, top_context, inline, domain, await_promise):
+
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete",
+ )
+
+ all_contexts = await bidi_session.browsing_context.get_tree()
+ iframe_context = all_contexts[0]["children"][0]
+
+ # This is equivalent to `document.getElementsByTagName("iframe")[0].conten
+ result = await bidi_session.script.evaluate(
+ expression="window.frames[0]",
+ target=ContextTarget(top_context["context"]),
+ await_promise=await_promise,
+ )
+
+ recursive_compare(
+ {
+ "type": "window",
+ "value": {
+ "context": iframe_context["context"]
+ }
+ }, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("domain", ["", "alt"],
+ ids=["same_origin", "cross_origin"])
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_window_context_same_id_after_navigation(bidi_session,
+ top_context,
+ inline,
+ domain,
+ await_promise):
+
+ defaultOrigin = inline(f"{domain}")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=defaultOrigin, wait="complete")
+
+ url = inline(f"{domain}", domain=domain)
+
+ result = await bidi_session.script.evaluate(
+ expression="window",
+ target=ContextTarget(top_context["context"]),
+ await_promise=await_promise,
+ serialization_options=SerializationOptions(max_object_depth=1),
+ )
+
+ original_context = result['value']['context']
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=url,
+ wait="complete")
+
+ result = await bidi_session.script.evaluate(
+ expression="window",
+ target=ContextTarget(top_context["context"]),
+ await_promise=await_promise,
+ serialization_options=SerializationOptions(max_object_depth=1),
+ )
+
+ navigated_context_id = result['value']['context']
+
+ assert navigated_context_id == original_context
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_node.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_node.py
new file mode 100644
index 0000000000..a0bfd0d4c0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_node.py
@@ -0,0 +1,741 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, SerializationOptions
+
+from ... import any_string, recursive_compare
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ ( # basic
+ """
+ document.querySelector("br")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # attributes
+ """
+ document.querySelector("svg")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {
+ "svg:foo": "bar",
+ },
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "svg",
+ "namespaceURI": "http://www.w3.org/2000/svg",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # all children including non-element nodes
+ """
+ document.querySelector("#with-text-node")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-text-node"},
+ "childNodeCount": 1,
+ "children": [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 3,
+ "nodeValue": "Lorem",
+ }
+ }],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # children limited due to max depth
+ """
+ document.querySelector("#with-children")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "children": [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }, {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ( # not connected
+ """
+ document.createElement("div")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "children": [],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ),
+ ], ids=[
+ "basic",
+ "attributes",
+ "all_children",
+ "children_max_depth",
+ "not_connected",
+ ]
+)
+async def test_element_node(bidi_session, get_test_page, top_context, expression, expected):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document.querySelector("input#button").attributes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "localName": "id",
+ "namespaceURI": None,
+ "nodeType": 2,
+ "nodeValue": "button",
+ },
+ },
+ ), (
+ """
+ document.querySelector("svg").attributes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "localName": "foo",
+ "namespaceURI": "http://www.w3.org/2000/svg",
+ "nodeType": 2,
+ "nodeValue": "bar",
+ },
+ },
+ ),
+ ], ids=[
+ "basic",
+ "namespaceURI",
+ ]
+)
+async def test_attribute_node(bidi_session, get_test_page, top_context, expression, expected):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document.querySelector("#with-text-node").childNodes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 3,
+ "nodeValue": "Lorem",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_text_node(bidi_session, get_test_page, top_context, expression, expected):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document.querySelector("foo").childNodes[1]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 4,
+ "nodeValue": " < > & ",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_cdata_node(bidi_session, inline, new_tab, expression, expected):
+ xml_page = inline("""<foo>CDATA section: <![CDATA[ < > & ]]>.</foo>""", doctype="xml")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab['context'], url=xml_page, wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 7,
+ "nodeValue": "href='foo.css'",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_processing_instruction_node(
+ bidi_session, inline, new_tab, expression, expected
+):
+ xml_page = inline("""<foo></foo>""", doctype="xml")
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab['context'], url=xml_page, wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(new_tab["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document.querySelector("#with-comment").childNodes[0]
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 8,
+ "nodeValue": " Comment ",
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_comment_node(bidi_session, get_test_page, top_context, expression, expected):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 2,
+ "children": [{
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 10
+ }
+ }, {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 2,
+ "localName": "html",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }],
+ "nodeType": 9
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_document_node(bidi_session, get_test_page, top_context, expression, expected):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document.doctype
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "nodeType": 10,
+ }
+ }
+ ),
+ ], ids=[
+ "basic",
+ ]
+)
+async def test_doctype_node(bidi_session, get_test_page, top_context, expression, expected):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ document.querySelector("#custom-element").shadowRoot;
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 1,
+ "mode": "open",
+ "nodeType": 11
+ }
+ }
+ ),
+ (
+ """
+ new DocumentFragment();
+ """,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 0,
+ "children": [],
+ "nodeType": 11,
+ }
+ }
+ ),
+ ], ids=[
+ "shadowRoot",
+ "not connected"
+ ]
+)
+async def test_document_fragment_node(
+ bidi_session, get_test_page, top_context, expression, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ """
+ [document.querySelector("img")]
+ """,
+ {
+ "type": "array",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ],
+ },
+ ),
+ (
+ """
+ const map = new Map();
+ map.set(document.querySelector("img"), "elem");
+ map
+ """,
+ {
+ "type": "map",
+ "value": [[
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ },
+ {
+ "type": "string",
+ "value": "elem"
+ }
+ ]]
+ }
+ ),
+ (
+ """
+ const map = new Map();
+ map.set("elem", document.querySelector("img"));
+ map
+ """,
+ {
+ "type": "map",
+ "value": [[
+ "elem", {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }
+ ]]
+ }
+ ),
+ (
+ """
+ ({"elem": document.querySelector("img")})
+ """,
+ {
+ "type": "object",
+ "value": [
+ ["elem", {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ }]
+ ]
+ }
+ ),
+ (
+ """
+ const set = new Set();
+ set.add(document.querySelector("img"));
+ set
+ """,
+ {
+ "type": "set",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ },
+ },
+ ],
+ },
+ ),
+ ], ids=[
+ "array", "map-key", "map-value", "object", "set"
+ ]
+)
+async def test_node_embedded_within(
+ bidi_session, get_test_page, top_context, expression, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "expression, expected",
+ [
+ (
+ "document.getElementsByTagName('img')",
+ {
+ "type": "htmlcollection",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ },
+ ]
+ }
+ ),
+ (
+ "document.querySelectorAll('img')",
+ {
+ "type": "nodelist",
+ "value": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "img",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1
+ }
+ },
+ ]
+ }
+ ),
+ ], ids=[
+ "htmlcollection",
+ "nodelist"
+ ]
+)
+async def test_node_within_dom_collection(
+ bidi_session,
+ get_test_page,
+ top_context,
+ expression,
+ expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context['context'], url=get_test_page(), wait="complete"
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ serialization_options=SerializationOptions(max_dom_depth=1),
+ )
+
+ recursive_compare(expected, result)
+
+
+@pytest.mark.parametrize("shadow_root_mode", ["open", "closed"])
+@pytest.mark.asyncio
+async def test_custom_element_with_shadow_root(
+ bidi_session, get_test_page, top_context, shadow_root_mode
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(shadow_root_mode=shadow_root_mode),
+ wait="complete",
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression="""document.querySelector("#custom-element");""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare({
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {
+ "id": "custom-element",
+ },
+ "childNodeCount": 0,
+ "localName": "custom-element",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "childNodeCount": 1,
+ "mode": shadow_root_mode,
+ "nodeType": 11,
+ }
+ },
+ }
+ }, result)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_ownership.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_ownership.py
new file mode 100644
index 0000000000..4a417532af
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/result_ownership.py
@@ -0,0 +1,60 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+from ... import assert_handle
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_throw_exception(bidi_session, top_context, result_ownership, should_contain_handle):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression='throw {a:1}',
+ await_promise=False,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(exception.value.result["exceptionDetails"]["exception"], should_contain_handle)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_invalid_script(bidi_session, top_context, result_ownership, should_contain_handle):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression="))) !!@@## some invalid JS script (((",
+ await_promise=False,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(exception.value.result["exceptionDetails"]["exception"], should_contain_handle)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_rejected_promise(bidi_session, top_context, result_ownership, should_contain_handle):
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression="Promise.reject({a:1})",
+ await_promise=True,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(exception.value.result["exceptionDetails"]["exception"], should_contain_handle)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+@pytest.mark.parametrize("result_ownership, should_contain_handle",
+ [("root", True), ("none", False), (None, False)])
+async def test_return_value(bidi_session, top_context, await_promise, result_ownership, should_contain_handle):
+ result = await bidi_session.script.evaluate(
+ expression="Promise.resolve({a: {b:1}})",
+ await_promise=await_promise,
+ result_ownership=result_ownership,
+ target=ContextTarget(top_context["context"]))
+
+ assert_handle(result, should_contain_handle)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/sandbox.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/sandbox.py
new file mode 100644
index 0000000000..3a6771780d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/sandbox.py
@@ -0,0 +1,199 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, RealmTarget, ScriptEvaluateResultException
+
+from ... import any_int, any_string, recursive_compare
+from .. import any_stack_trace
+
+
+@pytest.mark.asyncio
+async def test_sandbox(bidi_session, new_tab):
+ # Make changes in window
+ await bidi_session.script.evaluate(
+ expression="window.foo = 1",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+
+ # Check that changes are not present in sandbox
+ result_in_sandbox = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+ assert result_in_sandbox == {"type": "undefined"}
+
+ # Make changes in sandbox
+ await bidi_session.script.evaluate(
+ expression="window.bar = 1",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+
+ # Make sure that changes are present in sandbox
+ result_in_sandbox = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+ assert result_in_sandbox == {"type": "number", "value": 1}
+
+ # Make sure that changes didn't leak from sandbox
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "undefined"}
+
+
+@pytest.mark.asyncio
+async def test_sandbox_with_empty_name(bidi_session, new_tab):
+ # An empty string as a `sandbox` means the default realm should be used.
+ await bidi_session.script.evaluate(
+ expression="window.foo = 'bar'",
+ target=ContextTarget(new_tab["context"], ""),
+ await_promise=True,
+ )
+
+ # Make sure that we can find the sandbox with the empty name.
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"], ""),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "bar"}
+
+ # Make sure that we can find the value in the default realm.
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "bar"}
+
+
+@pytest.mark.asyncio
+async def test_switch_sandboxes(bidi_session, new_tab):
+ # Test that sandboxes are retained when switching between them
+ await bidi_session.script.evaluate(
+ expression="window.foo = 1",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ await bidi_session.script.evaluate(
+ expression="window.foo = 2",
+ target=ContextTarget(new_tab["context"], "sandbox_2"),
+ await_promise=True,
+ )
+
+ result_in_sandbox_1 = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_1 == {"type": "number", "value": 1}
+
+ result_in_sandbox_2 = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"], "sandbox_2"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_2 == {"type": "number", "value": 2}
+
+
+@pytest.mark.asyncio
+async def test_sandbox_with_side_effects(bidi_session, new_tab):
+ # Make sure changing the node in sandbox will affect the other sandbox as well
+ await bidi_session.script.evaluate(
+ expression="document.querySelector('body').textContent = 'foo'",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ expected_value = {"type": "string", "value": "foo"}
+
+ result_in_sandbox_1 = await bidi_session.script.evaluate(
+ expression="document.querySelector('body').textContent",
+ target=ContextTarget(new_tab["context"], "sandbox_1"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_1 == expected_value
+
+ result_in_sandbox_2 = await bidi_session.script.evaluate(
+ expression="document.querySelector('body').textContent",
+ target=ContextTarget(new_tab["context"], "sandbox_2"),
+ await_promise=True,
+ )
+ assert result_in_sandbox_2 == expected_value
+
+
+@pytest.mark.asyncio
+async def test_sandbox_returns_same_node(bidi_session, new_tab):
+ node = await bidi_session.script.evaluate(
+ expression="document.querySelector('body')",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ recursive_compare({"type": "node", "sharedId": any_string}, node)
+
+ node_sandbox = await bidi_session.script.evaluate(
+ expression="document.querySelector('body')",
+ target=ContextTarget(new_tab["context"], sandbox="sandbox_1"),
+ await_promise=True,
+ )
+ assert node_sandbox == node
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("await_promise", [True, False])
+async def test_exception_details(bidi_session, new_tab, await_promise):
+ if await_promise:
+ expression = "Promise.reject(1)"
+ else:
+ expression = "throw 1"
+
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression=expression,
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=await_promise,
+ )
+
+ recursive_compare(
+ {
+ "realm": any_string,
+ "exceptionDetails": {
+ "columnNumber": any_int,
+ "exception": {"type": "number", "value": 1},
+ "lineNumber": any_int,
+ "stackTrace": any_stack_trace,
+ "text": any_string,
+ },
+ },
+ exception.value.result,
+ )
+
+
+@pytest.mark.asyncio
+async def test_target_realm(bidi_session, top_context, default_realm):
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo = 3",
+ target=ContextTarget(top_context["context"], "sandbox"),
+ await_promise=True,
+ )
+ realm = result["realm"]
+
+ # Make sure that sandbox realm id is different from default
+ assert realm != default_realm
+
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo",
+ target=RealmTarget(realm),
+ await_promise=True,
+ )
+
+ recursive_compare(
+ {"realm": realm, "result": {"type": "number", "value": 3}}, result
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/serialization_options.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/serialization_options.py
new file mode 100644
index 0000000000..00e1703dc4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/serialization_options.py
@@ -0,0 +1,569 @@
+import pytest
+from webdriver.bidi.modules.script import ContextTarget, SerializationOptions
+
+from ... import any_string, recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "include_shadow_tree, shadow_root_mode, contains_children, expected",
+ [
+ (
+ None,
+ "open",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ None,
+ "closed",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "none",
+ "open",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "none",
+ "closed",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "open",
+ "open",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 11,
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "open",
+ },
+ },
+ ),
+ (
+ "open",
+ "closed",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"nodeType": 11, "childNodeCount": 1},
+ },
+ ),
+ (
+ "all",
+ "open",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 11,
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "open",
+ },
+ },
+ ),
+ (
+ "all",
+ "closed",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 11,
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "closed",
+ },
+ },
+ ),
+ ],
+ ids=[
+ "default mode for open shadow root",
+ "default mode for closed shadow root",
+ "'none' mode for open shadow root",
+ "'none' mode for closed shadow root",
+ "'open' mode for open shadow root",
+ "'open' mode for closed shadow root",
+ "'all' mode for open shadow root",
+ "'all' mode for closed shadow root",
+ ],
+)
+async def test_include_shadow_tree_for_custom_element(
+ bidi_session,
+ top_context,
+ get_test_page,
+ include_shadow_tree,
+ shadow_root_mode,
+ contains_children,
+ expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(shadow_root_mode=shadow_root_mode),
+ wait="complete",
+ )
+ result = await bidi_session.script.evaluate(
+ expression="""document.querySelector("custom-element")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(
+ include_shadow_tree=include_shadow_tree, max_dom_depth=1
+ ),
+ )
+
+ recursive_compare(expected, result["value"]["shadowRoot"])
+
+ # Explicitely check for children because recursive_compare skips it
+ if not contains_children:
+ assert "children" not in result["value"]["shadowRoot"]["value"]
+
+
+@pytest.mark.parametrize(
+ "include_shadow_tree, contains_children, expected",
+ [
+ (
+ None,
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"childNodeCount": 1, "mode": "open", "nodeType": 11},
+ },
+ ),
+ (
+ "none",
+ False,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {"childNodeCount": 1, "mode": "open", "nodeType": 11},
+ },
+ ),
+ (
+ "open",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "nodeType": 11,
+ "mode": "open",
+ },
+ },
+ ),
+ (
+ "all",
+ True,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "attributes": {"id": "in-shadow-dom"},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "mode": "open",
+ "nodeType": 11,
+ },
+ },
+ ),
+ ],
+ ids=[
+ "default mode",
+ "'none' mode",
+ "'open' mode",
+ "'all' mode",
+ ],
+)
+async def test_include_shadow_tree_for_shadow_root(
+ bidi_session,
+ top_context,
+ get_test_page,
+ include_shadow_tree,
+ contains_children,
+ expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=get_test_page(),
+ wait="complete",
+ )
+ result = await bidi_session.script.evaluate(
+ expression="""document.querySelector("custom-element").shadowRoot""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(
+ include_shadow_tree=include_shadow_tree, max_dom_depth=1
+ ),
+ )
+
+ recursive_compare(expected, result)
+
+ # Explicitely check for children because recursive_compare skips it
+ if not contains_children:
+ assert "children" not in result["value"]
+
+
+@pytest.mark.parametrize(
+ "max_dom_depth, expected",
+ [
+ (
+ None,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ (
+ 0,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ (
+ 1,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "children": [
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ (
+ 2,
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "attributes": {"id": "with-children"},
+ "childNodeCount": 2,
+ "children": [
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 0,
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ {
+ "sharedId": any_string,
+ "type": "node",
+ "value": {
+ "attributes": {},
+ "childNodeCount": 0,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ],
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "nodeType": 1,
+ "shadowRoot": None,
+ },
+ },
+ ),
+ ],
+)
+async def test_max_dom_depth(
+ bidi_session, top_context, get_test_page, max_dom_depth, expected
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=get_test_page(), wait="complete"
+ )
+ result = await bidi_session.script.evaluate(
+ expression="""document.querySelector("div#with-children")""",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(max_dom_depth=max_dom_depth),
+ )
+
+ recursive_compare(expected, result)
+
+
+async def test_max_dom_depth_null(
+ bidi_session,
+ send_blocking_command,
+ top_context,
+ get_test_page,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=get_test_page(), wait="complete"
+ )
+ result = await send_blocking_command(
+ "script.evaluate",
+ {
+ "expression": """document.querySelector("div#with-children")""",
+ "target": ContextTarget(top_context["context"]),
+ "awaitPromise": True,
+ "serializationOptions": {"maxDomDepth": None},
+ },
+ )
+
+ recursive_compare(
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "div",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 2,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "p",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 1,
+ "children": [
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "span",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 0,
+ "children": [],
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ }
+ ],
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ },
+ {
+ "type": "node",
+ "sharedId": any_string,
+ "value": {
+ "nodeType": 1,
+ "localName": "br",
+ "namespaceURI": "http://www.w3.org/1999/xhtml",
+ "childNodeCount": 0,
+ "children": [],
+ "attributes": {},
+ "shadowRoot": None,
+ },
+ },
+ ],
+ "attributes": {"id": "with-children"},
+ "shadowRoot": None,
+ },
+ },
+ result["result"],
+ )
+
+
+@pytest.mark.parametrize(
+ "max_object_depth, expected",
+ [
+ (
+ None,
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "array", "value": [{"type": "number", "value": 2}]},
+ ],
+ },
+ ),
+ (0, {"type": "array"}),
+ (
+ 1,
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "array"},
+ ],
+ },
+ ),
+ (
+ 2,
+ {
+ "type": "array",
+ "value": [
+ {"type": "number", "value": 1},
+ {"type": "array", "value": [{"type": "number", "value": 2}]},
+ ],
+ },
+ ),
+ ],
+)
+async def test_max_object_depth(bidi_session, top_context, max_object_depth, expected):
+ result = await bidi_session.script.evaluate(
+ expression="[1, [2]]",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ serialization_options=SerializationOptions(max_object_depth=max_object_depth),
+ )
+
+ assert result == expected
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/strict_mode.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/strict_mode.py
new file mode 100644
index 0000000000..386d03b08d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/strict_mode.py
@@ -0,0 +1,34 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException
+from ... import recursive_compare
+from .. import specific_error_response
+
+
+@pytest.mark.asyncio
+async def test_strict_mode(bidi_session, top_context):
+ # As long as there is no `SOME_VARIABLE`, the command should fail in strict mode.
+ with pytest.raises(ScriptEvaluateResultException) as exception:
+ await bidi_session.script.evaluate(
+ expression="'use strict';SOME_VARIABLE=1",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+ recursive_compare(specific_error_response({"type": "error"}), exception.value.result)
+
+ # In non-strict mode, the command should succeed and global `SOME_VARIABLE` should be created.
+ result = await bidi_session.script.evaluate(
+ expression="SOME_VARIABLE=1",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+ assert result == {
+ "type": "number",
+ "value": 1}
+
+ # Access created by the previous command `SOME_VARIABLE`.
+ result = await bidi_session.script.evaluate(
+ expression="'use strict';SOME_VARIABLE=1",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+ assert result == {
+ "type": "number",
+ "value": 1}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/target.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/target.py
new file mode 100644
index 0000000000..e67a5dd81f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/target.py
@@ -0,0 +1,33 @@
+import pytest
+
+from webdriver.bidi.modules.script import (
+ ContextTarget,
+)
+
+from ... import recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_target_context_and_realm(bidi_session, top_context, new_tab):
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo = 3",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ )
+ realm = result["realm"]
+
+ # Make sure that realm argument is ignored and
+ # script is executed in the right context.
+ result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="window.foo",
+ target={"context": new_tab["context"], "realm": realm},
+ await_promise=True,
+ )
+
+ assert realm != result["realm"]
+ recursive_compare(
+ {"realm": result["realm"], "result": {"type": "undefined"}}, result
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/user_activation.py b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/user_activation.py
new file mode 100644
index 0000000000..cc1f27985e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/evaluate/user_activation.py
@@ -0,0 +1,41 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("user_activation", [True, False])
+async def test_userActivation(bidi_session, top_context, user_activation):
+ # Consume any previously set activation.
+ await bidi_session.script.evaluate(expression="""window.open();""",
+ target=ContextTarget(
+ top_context["context"]),
+ await_promise=False)
+
+ result = await bidi_session.script.evaluate(
+ expression=
+ "navigator.userActivation.isActive && navigator.userActivation.hasBeenActive",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ user_activation=user_activation)
+
+ assert result == {"type": "boolean", "value": user_activation}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("user_activation", [True, False])
+async def test_userActivation_copy(bidi_session, top_context, user_activation):
+ # Consume any previously set activation.
+ await bidi_session.script.evaluate(expression="""window.open();""",
+ target=ContextTarget(
+ top_context["context"]),
+ await_promise=False)
+
+ result = await bidi_session.script.evaluate(
+ expression=
+ "document.body.appendChild(document.createTextNode('test')) && document.execCommand('selectAll') && document.execCommand('copy')",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True,
+ user_activation=user_activation)
+
+ assert result == {"type": "boolean", "value": user_activation}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/context.py b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/context.py
new file mode 100644
index 0000000000..1d765c7b4a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/context.py
@@ -0,0 +1,70 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import recursive_compare
+
+
+@pytest.mark.asyncio
+async def test_context(
+ bidi_session,
+ test_alt_origin,
+ test_origin,
+ test_page_cross_origin_frame,
+):
+ new_context = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=test_page_cross_origin_frame,
+ wait="complete",
+ )
+
+ # Evaluate to get realm id
+ new_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(new_context["context"]),
+ await_promise=False,
+ )
+
+ result = await bidi_session.script.get_realms(context=new_context["context"])
+
+ recursive_compare(
+ [
+ {
+ "context": new_context["context"],
+ "origin": test_origin,
+ "realm": new_context_result["realm"],
+ "type": "window",
+ },
+ ],
+ result,
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_context["context"])
+ assert len(contexts) == 1
+ frames = contexts[0]["children"]
+ assert len(frames) == 1
+ frame_context = frames[0]["context"]
+
+ # Evaluate to get realm id
+ frame_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(frame_context),
+ await_promise=False,
+ )
+
+ result = await bidi_session.script.get_realms(context=frame_context)
+
+ recursive_compare(
+ [
+ {
+ "context": frame_context,
+ "origin": test_alt_origin,
+ "realm": frame_context_result["realm"],
+ "type": "window",
+ },
+ ],
+ result,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/get_realms.py b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/get_realms.py
new file mode 100644
index 0000000000..4dfce5ab49
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/get_realms.py
@@ -0,0 +1,183 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import any_string, recursive_compare
+
+PAGE_ABOUT_BLANK = "about:blank"
+
+
+@pytest.mark.asyncio
+async def test_payload_types(bidi_session):
+ result = await bidi_session.script.get_realms()
+
+ recursive_compare(
+ [
+ {
+ "context": any_string,
+ "origin": any_string,
+ "realm": any_string,
+ "type": any_string,
+ }
+ ],
+ result,
+ )
+
+
+@pytest.mark.asyncio
+async def test_realm_is_consistent_when_calling_twice(bidi_session):
+ result = await bidi_session.script.get_realms()
+
+ result_calling_again = await bidi_session.script.get_realms()
+
+ assert result[0]["realm"] == result_calling_again[0]["realm"]
+
+
+@pytest.mark.asyncio
+async def test_realm_is_different_after_reload(bidi_session, top_context):
+ result = await bidi_session.script.get_realms()
+
+ # Reload the page
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=PAGE_ABOUT_BLANK, wait="complete"
+ )
+
+ result_after_reload = await bidi_session.script.get_realms()
+
+ assert result[0]["realm"] != result_after_reload[0]["realm"]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_multiple_top_level_contexts(bidi_session, top_context, type_hint):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ result = await bidi_session.script.get_realms()
+
+ # Evaluate to get realm ids
+ top_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+ new_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(new_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(
+ [
+ {
+ "context": top_context["context"],
+ "origin": "null",
+ "realm": top_context_result["realm"],
+ "type": "window",
+ },
+ {
+ "context": new_context["context"],
+ "origin": "null",
+ "realm": new_context_result["realm"],
+ "type": "window",
+ },
+ ],
+ result,
+ )
+
+
+@pytest.mark.asyncio
+async def test_iframes(
+ bidi_session,
+ top_context,
+ test_alt_origin,
+ test_origin,
+ test_page_cross_origin_frame,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=test_page_cross_origin_frame,
+ wait="complete",
+ )
+
+ result = await bidi_session.script.get_realms()
+
+ # Evaluate to get realm id
+ top_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ assert len(contexts) == 1
+ frames = contexts[0]["children"]
+ assert len(frames) == 1
+ frame_context = frames[0]["context"]
+
+ # Evaluate to get realm id
+ frame_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(frame_context),
+ await_promise=False,
+ )
+
+ recursive_compare(
+ [
+ {
+ "context": top_context["context"],
+ "origin": test_origin,
+ "realm": top_context_result["realm"],
+ "type": "window",
+ },
+ {
+ "context": frame_context,
+ "origin": test_alt_origin,
+ "realm": frame_context_result["realm"],
+ "type": "window",
+ },
+ ],
+ result,
+ )
+
+ # Clean up origin
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=PAGE_ABOUT_BLANK, wait="complete"
+ )
+
+
+@pytest.mark.asyncio
+async def test_origin(bidi_session, inline, top_context, test_origin):
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ result = await bidi_session.script.get_realms()
+
+ # Evaluate to get realm id
+ top_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(
+ [
+ {
+ "context": top_context["context"],
+ "origin": test_origin,
+ "realm": top_context_result["realm"],
+ "type": "window",
+ }
+ ],
+ result,
+ )
+
+ # Clean up origin
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=PAGE_ABOUT_BLANK, wait="complete"
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/invalid.py
new file mode 100644
index 0000000000..c15378a6e0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/invalid.py
@@ -0,0 +1,26 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("context", [False, 42, {}, []])
+async def test_params_context_invalid_type(bidi_session, context):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.get_realms(context=context)
+
+
+async def test_params_context_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.script.get_realms(context="foo")
+
+
+@pytest.mark.parametrize("type", [False, 42, {}, []])
+async def test_params_type_invalid_type(bidi_session, type):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.get_realms(type=type)
+
+
+async def test_params_type_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.get_realms(type="foo")
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/sandbox.py b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/sandbox.py
new file mode 100644
index 0000000000..6ce1fee552
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/sandbox.py
@@ -0,0 +1,238 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import recursive_compare
+
+PAGE_ABOUT_BLANK = "about:blank"
+
+
+@pytest.mark.asyncio
+async def test_sandbox(bidi_session, top_context):
+ evaluate_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ # Create a sandbox
+ evaluate_in_sandbox_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"], "sandbox"),
+ await_promise=False,
+ )
+
+ result = await bidi_session.script.get_realms()
+
+ recursive_compare(
+ [
+ {
+ "context": top_context["context"],
+ "origin": "null",
+ "realm": evaluate_result["realm"],
+ "type": "window",
+ },
+ {
+ "context": top_context["context"],
+ "origin": "null",
+ "realm": evaluate_in_sandbox_result["realm"],
+ "sandbox": "sandbox",
+ "type": "window",
+ },
+ ],
+ result,
+ )
+
+ # Reload to clean up sandboxes
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=PAGE_ABOUT_BLANK, wait="complete"
+ )
+
+
+@pytest.mark.asyncio
+async def test_origin(bidi_session, inline, top_context, test_origin):
+ url = inline("<div>foo</div>")
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=url, wait="complete"
+ )
+
+ evaluate_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ # Create a sandbox
+ evaluate_in_sandbox_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"], "sandbox"),
+ await_promise=False,
+ )
+
+ result = await bidi_session.script.get_realms()
+
+ recursive_compare(
+ [
+ {
+ "context": top_context["context"],
+ "origin": test_origin,
+ "realm": evaluate_result["realm"],
+ "type": "window",
+ },
+ {
+ "context": top_context["context"],
+ "origin": test_origin,
+ "realm": evaluate_in_sandbox_result["realm"],
+ "sandbox": "sandbox",
+ "type": "window",
+ },
+ ],
+ result,
+ )
+
+ # Reload to clean up sandboxes
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=PAGE_ABOUT_BLANK, wait="complete"
+ )
+
+
+@pytest.mark.asyncio
+async def test_type(bidi_session, top_context):
+ evaluate_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ # Create a sandbox
+ evaluate_in_sandbox_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"], "sandbox"),
+ await_promise=False,
+ )
+
+ # Should be extended when more types are supported
+ result = await bidi_session.script.get_realms(type="window")
+
+ recursive_compare(
+ [
+ {
+ "context": top_context["context"],
+ "origin": "null",
+ "realm": evaluate_result["realm"],
+ "type": "window",
+ },
+ {
+ "context": top_context["context"],
+ "origin": "null",
+ "realm": evaluate_in_sandbox_result["realm"],
+ "sandbox": "sandbox",
+ "type": "window",
+ },
+ ],
+ result,
+ )
+
+ # Reload to clean up sandboxes
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=PAGE_ABOUT_BLANK, wait="complete"
+ )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_multiple_top_level_contexts(
+ bidi_session,
+ test_alt_origin,
+ test_origin,
+ test_page_cross_origin_frame,
+ type_hint,
+):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"],
+ url=test_page_cross_origin_frame,
+ wait="complete",
+ )
+
+ evaluate_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(new_context["context"]),
+ await_promise=False,
+ )
+
+ # Create a sandbox
+ evaluate_in_sandbox_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(new_context["context"], "sandbox"),
+ await_promise=False,
+ )
+
+ result = await bidi_session.script.get_realms(context=new_context["context"])
+ recursive_compare(
+ [
+ {
+ "context": new_context["context"],
+ "origin": test_origin,
+ "realm": evaluate_result["realm"],
+ "type": "window",
+ },
+ {
+ "context": new_context["context"],
+ "origin": test_origin,
+ "realm": evaluate_in_sandbox_result["realm"],
+ "sandbox": "sandbox",
+ "type": "window",
+ },
+ ],
+ result,
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_context["context"])
+ assert len(contexts) == 1
+ frames = contexts[0]["children"]
+ assert len(frames) == 1
+ frame_context = frames[0]["context"]
+
+ evaluate_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(frame_context),
+ await_promise=False,
+ )
+
+ # Create a sandbox in iframe
+ evaluate_in_sandbox_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(frame_context, "sandbox"),
+ await_promise=False,
+ )
+
+ result = await bidi_session.script.get_realms(context=frame_context)
+ recursive_compare(
+ [
+ {
+ "context": frame_context,
+ "origin": test_alt_origin,
+ "realm": evaluate_result["realm"],
+ "type": "window",
+ },
+ {
+ "context": frame_context,
+ "origin": test_alt_origin,
+ "realm": evaluate_in_sandbox_result["realm"],
+ "sandbox": "sandbox",
+ "type": "window",
+ },
+ ],
+ result,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/type.py b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/type.py
new file mode 100644
index 0000000000..7a8b4d43b7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/get_realms/type.py
@@ -0,0 +1,34 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+from ... import recursive_compare
+
+PAGE_ABOUT_BLANK = "about:blank"
+
+
+@pytest.mark.asyncio
+# Should be extended when more types are supported
+@pytest.mark.parametrize("type", ["window"])
+async def test_type(bidi_session, top_context, type):
+ result = await bidi_session.script.get_realms(type=type)
+
+ # Evaluate to get realm id
+ top_context_result = await bidi_session.script.evaluate(
+ raw_result=True,
+ expression="1 + 2",
+ target=ContextTarget(top_context["context"]),
+ await_promise=False,
+ )
+
+ recursive_compare(
+ [
+ {
+ "context": top_context["context"],
+ "origin": "null",
+ "realm": top_context_result["realm"],
+ "type": type,
+ }
+ ],
+ result,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/message/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/message/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/message/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/message/message.py b/testing/web-platform/tests/webdriver/tests/bidi/script/message/message.py
new file mode 100644
index 0000000000..bd5c9a111f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/message/message.py
@@ -0,0 +1,101 @@
+import pytest
+from tests.support.sync import AsyncPoll
+
+from webdriver.bidi.modules.script import ContextTarget
+from webdriver.error import TimeoutException
+
+
+pytestmark = pytest.mark.asyncio
+
+MESSAGE_EVENT = "script.message"
+
+
+async def test_unsubscribe(bidi_session, top_context):
+ await bidi_session.session.subscribe(events=[MESSAGE_EVENT])
+ await bidi_session.session.unsubscribe(events=[MESSAGE_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(MESSAGE_EVENT, on_event)
+
+ await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="(channel) => channel('foo')",
+ arguments=[{"type": "channel", "value": {"channel": "channel_name"}}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ assert len(events) == 0
+
+ remove_listener()
+
+
+async def test_subscribe(bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe):
+ await subscribe_events(events=[MESSAGE_EVENT])
+
+ on_script_message = wait_for_event(MESSAGE_EVENT)
+ result = await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="(channel) => channel('foo')",
+ arguments=[{"type": "channel", "value": {"channel": "channel_name"}}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+ event = await wait_for_future_safe(on_script_message)
+
+ assert event == {
+ "channel": "channel_name",
+ "data": {"type": "string", "value": "foo"},
+ "source": {
+ "realm": result["realm"],
+ "context": top_context["context"],
+ },
+ }
+
+
+async def test_subscribe_to_one_context(
+ bidi_session, subscribe_events, top_context, new_tab
+):
+ # Subscribe to a specific context
+ await subscribe_events(
+ events=[MESSAGE_EVENT], contexts=[top_context["context"]]
+ )
+
+ # Track all received script.message events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(MESSAGE_EVENT, on_event)
+
+ # Send the event in the other context
+ await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="(channel) => channel('foo')",
+ arguments=[{"type": "channel", "value": {"channel": "channel_name"}}],
+ await_promise=False,
+ target=ContextTarget(new_tab["context"]),
+ )
+
+ # Make sure we didn't receive the event for the new tab
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ await bidi_session.script.call_function(
+ raw_result=True,
+ function_declaration="(channel) => channel('foo')",
+ arguments=[{"type": "channel", "value": {"channel": "channel_name"}}],
+ await_promise=False,
+ target=ContextTarget(top_context["context"]),
+ )
+
+ # Make sure we received the event for the right context
+ assert len(events) == 1
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/realm_created.py b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/realm_created.py
new file mode 100644
index 0000000000..f4dc681a3a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_created/realm_created.py
@@ -0,0 +1,365 @@
+import pytest
+from tests.support.sync import AsyncPoll
+
+from webdriver.bidi.modules.script import RealmTarget
+from webdriver.error import TimeoutException
+from ... import any_string, recursive_compare
+from .. import create_sandbox
+
+
+pytestmark = pytest.mark.asyncio
+
+REALM_CREATED_EVENT = "script.realmCreated"
+
+
+async def test_unsubscribe(bidi_session):
+ await bidi_session.session.subscribe(events=[REALM_CREATED_EVENT])
+ await bidi_session.session.unsubscribe(events=[REALM_CREATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.create(type_hint="tab")
+
+ assert len(events) == 0
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["window", "tab"])
+async def test_create_context(bidi_session, subscribe_events, type_hint):
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ wait = AsyncPoll(bidi_session, message="Didn't receive realm created events")
+ await wait.until(lambda _: len(events) >= 1)
+
+ result = await bidi_session.script.get_realms(context=new_context["context"])
+
+ assert events[-1] == result[0]
+
+ remove_listener()
+
+
+async def test_navigate(bidi_session, subscribe_events, inline, new_tab):
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ result = await bidi_session.script.get_realms(context=new_tab["context"])
+
+ assert events[-1] == result[0]
+
+ remove_listener()
+
+
+async def test_reload(bidi_session, subscribe_events, new_tab, inline):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.reload(
+ context=new_tab["context"], wait="complete"
+ )
+
+ result = await bidi_session.script.get_realms(context=new_tab["context"])
+
+ assert events[-1] == result[0]
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("method", ["evaluate", "call_function"])
+async def test_sandbox(
+ bidi_session, subscribe_events, new_tab, wait_for_event, wait_for_future_safe, test_origin, method
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_origin, wait="complete"
+ )
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ on_realm_created = wait_for_event(REALM_CREATED_EVENT)
+
+ sandbox_name = "Test"
+ sandbox_realm = await create_sandbox(
+ bidi_session, new_tab["context"], sandbox_name, method
+ )
+
+ event = await wait_for_future_safe(on_realm_created)
+
+ assert event == {
+ "context": new_tab["context"],
+ "origin": test_origin,
+ "realm": sandbox_realm,
+ "sandbox": sandbox_name,
+ "type": "window",
+ }
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_iframe(bidi_session, subscribe_events, top_context, inline, domain):
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+
+ realms = await bidi_session.script.get_realms()
+
+ for realm in realms:
+ # Find the relevant event for the specific realm
+ event = [item for item in events if item["realm"] == realm["realm"]]
+ assert event[0] == realm
+
+ remove_listener()
+
+
+async def test_subscribe_to_one_context(
+ bidi_session, subscribe_events, new_tab, inline, top_context
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ # Subscribe to a specific context
+ await subscribe_events(events=[REALM_CREATED_EVENT], contexts=[new_tab["context"]])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ # Make sure we didn't receive the event for the top context
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ result = await bidi_session.script.get_realms(context=new_tab["context"])
+
+ assert events[-1] == result[0]
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("method", ["evaluate", "call_function"])
+async def test_script_when_realm_is_created(
+ bidi_session, subscribe_events, new_tab, wait_for_event,wait_for_future_safe, inline, method
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ on_realm_created = wait_for_event(REALM_CREATED_EVENT)
+
+ await bidi_session.browsing_context.reload(context=new_tab["context"])
+
+ realm_info = await wait_for_future_safe(on_realm_created)
+
+ # Validate that it's possible to execute the script
+ # as soon as a realm is created.
+ if method == "evaluate":
+ result = await bidi_session.script.evaluate(
+ expression="1 + 2",
+ await_promise=False,
+ target=RealmTarget(realm_info["realm"]),
+ )
+ else:
+ result = await bidi_session.script.call_function(
+ function_declaration="() => 1 + 2",
+ await_promise=False,
+ target=RealmTarget(realm_info["realm"]),
+ )
+
+ assert result == {"type": "number", "value": 3}
+
+
+async def test_dedicated_worker(
+ wait_for_future_safe,
+ bidi_session,
+ subscribe_events,
+ top_context,
+ inline,
+ event_loop,
+):
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ window_realm = None
+ worker_realm = event_loop.create_future()
+
+ async def on_event(method, data):
+ if data["type"] == "dedicated-worker":
+ if worker_realm.done():
+ raise "More than one dedicated worker"
+ else:
+ worker_realm.set_result(data)
+ elif data["type"] == "window":
+ nonlocal window_realm
+ window_realm = data
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ worker_url = inline("while(true){}", doctype="js")
+ url = inline(f"<script>const worker = new Worker('{worker_url}');</script>")
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+
+ realm = await wait_for_future_safe(worker_realm)
+ remove_listener()
+
+ recursive_compare(
+ {
+ "type": "dedicated-worker",
+ "realm": any_string,
+ "origin": worker_url,
+ "owners": [window_realm["realm"]],
+ },
+ realm,
+ )
+
+
+async def test_shared_worker(
+ wait_for_future_safe,
+ bidi_session,
+ subscribe_events,
+ top_context,
+ inline,
+ event_loop,
+):
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ window_realm = None
+ worker_realm = event_loop.create_future()
+
+ async def on_event(method, data):
+ if data["type"] == "shared-worker":
+ if worker_realm.done():
+ raise "More than one shared worker"
+ else:
+ worker_realm.set_result(data)
+ elif data["type"] == "window":
+ nonlocal window_realm
+ window_realm = data
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ worker_url = inline("while(true){}", doctype="js")
+ url = inline(
+ f"""<script>
+ const worker = new SharedWorker('{worker_url}');
+ </script>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+
+ realm = await wait_for_future_safe(worker_realm)
+ remove_listener()
+
+ recursive_compare(
+ {
+ "type": "shared-worker",
+ "realm": any_string,
+ "origin": worker_url,
+ },
+ realm,
+ )
+
+
+async def test_service_worker(
+ wait_for_future_safe,
+ bidi_session,
+ subscribe_events,
+ top_context,
+ inline,
+ event_loop,
+):
+ await subscribe_events(events=[REALM_CREATED_EVENT])
+
+ window_realm = None
+ worker_realm = event_loop.create_future()
+
+ async def on_event(method, data):
+ if data["type"] == "service-worker":
+ if worker_realm.done():
+ raise "More than one service worker"
+ else:
+ worker_realm.set_result(data)
+ elif data["type"] == "window":
+ nonlocal window_realm
+ window_realm = data
+
+ remove_listener = bidi_session.add_event_listener(REALM_CREATED_EVENT, on_event)
+
+ worker_url = inline("while(true){}", doctype="js")
+ url = inline(
+ f"""<script>
+ navigator.serviceWorker.register('{worker_url}');
+ navigator.serviceWorker.startMessages();
+ </script>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+
+ realm = await wait_for_future_safe(worker_realm)
+ remove_listener()
+
+ recursive_compare(
+ {
+ "type": "service-worker",
+ "realm": any_string,
+ "origin": worker_url,
+ },
+ realm,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/realm_destroyed.py b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/realm_destroyed.py
new file mode 100644
index 0000000000..ac3a67ec74
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/realm_destroyed/realm_destroyed.py
@@ -0,0 +1,342 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.error import TimeoutException
+from ..realm_created.realm_created import REALM_CREATED_EVENT
+
+from .. import create_sandbox
+
+
+pytestmark = pytest.mark.asyncio
+
+REALM_DESTROYED_EVENT = "script.realmDestroyed"
+
+
+async def test_unsubscribe(bidi_session):
+ new_context = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.session.subscribe(events=[REALM_DESTROYED_EVENT])
+ await bidi_session.session.unsubscribe(events=[REALM_DESTROYED_EVENT])
+
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener(REALM_DESTROYED_EVENT, on_event)
+
+ await bidi_session.browsing_context.close(context=new_context["context"])
+
+ assert len(events) == 0
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("type_hint", ["window", "tab"])
+async def test_close_context(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, type_hint):
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+ await subscribe_events(events=[REALM_DESTROYED_EVENT])
+
+ result = await bidi_session.script.get_realms(context=new_context["context"])
+
+ on_realm_destroyed = wait_for_event(REALM_DESTROYED_EVENT)
+
+ await bidi_session.browsing_context.close(context=new_context["context"])
+
+ event = await wait_for_future_safe(on_realm_destroyed)
+
+ assert event == {"realm": result[0]["realm"]}
+
+
+async def test_navigate(
+ bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, inline, new_tab
+):
+ await subscribe_events(events=[REALM_DESTROYED_EVENT])
+
+ result = await bidi_session.script.get_realms(context=new_tab["context"])
+
+ on_realm_destroyed = wait_for_event(REALM_DESTROYED_EVENT)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ event = await wait_for_future_safe(on_realm_destroyed)
+
+ assert event == {"realm": result[0]["realm"]}
+
+
+async def test_reload_context(
+ bidi_session, subscribe_events, wait_for_event, wait_for_future_safe, top_context
+):
+ await subscribe_events(events=[REALM_DESTROYED_EVENT])
+
+ result = await bidi_session.script.get_realms(context=top_context["context"])
+
+ on_realm_destroyed = wait_for_event(REALM_DESTROYED_EVENT)
+
+ await bidi_session.browsing_context.reload(context=top_context["context"])
+
+ event = await wait_for_future_safe(on_realm_destroyed)
+
+ assert event == {"realm": result[0]["realm"]}
+
+
+@pytest.mark.parametrize("method", ["evaluate", "call_function"])
+async def test_sandbox(bidi_session, subscribe_events, new_tab, method):
+ await subscribe_events(events=[REALM_DESTROYED_EVENT])
+
+ # Track all received script.realmDestroyed events in the destroyed_realm_ids array
+ destroyed_realm_ids = []
+
+ async def on_event(method, data):
+ destroyed_realm_ids.append(data["realm"])
+
+ remove_listener = bidi_session.add_event_listener(REALM_DESTROYED_EVENT, on_event)
+
+ sandbox_realm = await create_sandbox(
+ bidi_session, new_tab["context"], "test", method
+ )
+
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ wait = AsyncPoll(bidi_session, message="Didn't receive realm destroyed events")
+ await wait.until(lambda _: len(destroyed_realm_ids) >= 2)
+
+ assert sandbox_realm in destroyed_realm_ids
+
+ remove_listener()
+
+
+async def test_subscribe_after_sandbox_creation(
+ bidi_session, subscribe_events, new_tab, inline
+):
+ sandbox_realm = await create_sandbox(bidi_session, new_tab["context"])
+
+ await subscribe_events(events=[REALM_DESTROYED_EVENT])
+
+ # Track all received script.realmDestroyed events in the destroyed_realm_ids array
+ destroyed_realm_ids = []
+
+ async def on_event(method, data):
+ destroyed_realm_ids.append(data["realm"])
+
+ remove_listener = bidi_session.add_event_listener(REALM_DESTROYED_EVENT, on_event)
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ wait = AsyncPoll(bidi_session, message="Didn't receive realm destroyed events")
+ await wait.until(lambda _: len(destroyed_realm_ids) >= 2)
+
+ assert sandbox_realm in destroyed_realm_ids
+
+ remove_listener()
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_iframe(
+ bidi_session, subscribe_events, top_context, inline, wait_for_event, wait_for_future_safe, domain
+):
+ frame_url = inline("<div>foo</div>")
+ url = inline(f"<iframe src='{frame_url}'></iframe>", domain=domain)
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ await subscribe_events(events=[REALM_DESTROYED_EVENT])
+
+ on_realm_destroyed = wait_for_event(REALM_DESTROYED_EVENT)
+
+ frame_context = contexts[0]["children"][0]["context"]
+ result = await bidi_session.script.get_realms(context=frame_context)
+
+ await bidi_session.browsing_context.navigate(
+ context=frame_context, url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ event = await wait_for_future_safe(on_realm_destroyed)
+
+ assert event == {"realm": result[0]["realm"]}
+
+
+async def test_iframe_destroy_parent(
+ bidi_session, subscribe_events, test_page_same_origin_frame, new_tab
+):
+ await bidi_session.browsing_context.navigate(
+ url=test_page_same_origin_frame, context=new_tab["context"], wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+
+ await subscribe_events(events=[REALM_DESTROYED_EVENT])
+
+ # Track all received script.realmDestroyed events in the destroyed_realm_ids array
+ destroyed_realm_ids = []
+
+ async def on_event(method, data):
+ destroyed_realm_ids.append(data["realm"])
+
+ remove_listener = bidi_session.add_event_listener(REALM_DESTROYED_EVENT, on_event)
+
+ realm_for_iframe = await bidi_session.script.get_realms(
+ context=contexts[0]["children"][0]["context"]
+ )
+ realm_for_parent = await bidi_session.script.get_realms(context=new_tab["context"])
+
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ wait = AsyncPoll(bidi_session, message="Didn't receive realm destroyed events")
+ await wait.until(lambda _: len(destroyed_realm_ids) >= 2)
+
+ assert realm_for_iframe[0]["realm"] in destroyed_realm_ids
+ assert realm_for_parent[0]["realm"] in destroyed_realm_ids
+
+ remove_listener()
+
+
+async def test_subscribe_to_one_context(
+ bidi_session, subscribe_events, new_tab, inline, top_context
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ # Subscribe to a specific context
+ await subscribe_events(
+ events=[REALM_DESTROYED_EVENT], contexts=[new_tab["context"]]
+ )
+
+ # Track all received script.realmDestroyed events in the destroyed_realm_ids array
+ destroyed_realm_ids = []
+
+ async def on_event(method, data):
+ destroyed_realm_ids.append(data["realm"])
+
+ remove_listener = bidi_session.add_event_listener(REALM_DESTROYED_EVENT, on_event)
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ # Make sure we didn't receive the event for the top context
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(destroyed_realm_ids) > 0)
+
+ result = await bidi_session.script.get_realms(context=new_tab["context"])
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=inline("<div>foo</div>"), wait="complete"
+ )
+
+ wait = AsyncPoll(bidi_session, message="Didn't receive realm destroyed events")
+ await wait.until(lambda _: len(destroyed_realm_ids) >= 1)
+
+ assert result[0]["realm"] in destroyed_realm_ids
+
+ remove_listener()
+
+
+async def test_dedicated_worker(
+ wait_for_future_safe,
+ bidi_session,
+ subscribe_events,
+ top_context,
+ inline,
+ event_loop,
+):
+ await subscribe_events(events=[REALM_CREATED_EVENT, REALM_DESTROYED_EVENT])
+
+ found = event_loop.create_future()
+ worker_realm = event_loop.create_future()
+
+ async def on_realm_created_event(method, data):
+ if data["type"] == "dedicated-worker":
+ if worker_realm.done():
+ raise "More than one dedicated worker"
+ else:
+ worker_realm.set_result(data)
+
+ async def on_realm_destroyed_event(method, data):
+ if worker_realm.done() and data["realm"] == worker_realm.result()["realm"]:
+ found.set_result(True)
+
+ remove_realm_created_listener = bidi_session.add_event_listener(
+ REALM_CREATED_EVENT, on_realm_created_event
+ )
+ remove_realm_destroyed_listener = bidi_session.add_event_listener(
+ REALM_DESTROYED_EVENT, on_realm_destroyed_event
+ )
+
+ worker_url = inline("while(true){}", doctype="js")
+ url = inline(
+ f"""<script>
+ const worker = new Worker('{worker_url}');
+ setTimeout(() => {{
+ worker.terminate();
+ }}, 100);
+ </script>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+
+ assert await wait_for_future_safe(found)
+ remove_realm_created_listener()
+ remove_realm_destroyed_listener()
+
+
+async def test_shared_worker(
+ wait_for_future_safe,
+ bidi_session,
+ subscribe_events,
+ top_context,
+ inline,
+ event_loop,
+):
+ await subscribe_events(events=[REALM_CREATED_EVENT, REALM_DESTROYED_EVENT])
+
+ found = event_loop.create_future()
+ worker_realm = event_loop.create_future()
+
+ async def on_realm_created_event(method, data):
+ if data["type"] == "shared-worker":
+ if worker_realm.done():
+ raise "More than one dedicated worker"
+ else:
+ worker_realm.set_result(data)
+
+ async def on_realm_destroyed_event(method, data):
+ if worker_realm.done() and data["realm"] == worker_realm.result()["realm"]:
+ found.set_result(True)
+
+ remove_realm_created_listener = bidi_session.add_event_listener(
+ REALM_CREATED_EVENT, on_realm_created_event
+ )
+ remove_realm_destroyed_listener = bidi_session.add_event_listener(
+ REALM_DESTROYED_EVENT, on_realm_destroyed_event
+ )
+
+ worker_url = inline("while(true){}", doctype="js")
+ url = inline(
+ f"""<script>
+ const worker = new SharedWorker('{worker_url}');
+ </script>"""
+ )
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+ # Wait for the worker realm before navigating to ensure we aren't navigating
+ # too early.
+ assert await wait_for_future_safe(worker_realm)
+
+ url = inline("")
+ await bidi_session.browsing_context.navigate(
+ url=url, context=top_context["context"], wait="complete"
+ )
+ assert await wait_for_future_safe(found)
+
+ remove_realm_created_listener()
+ remove_realm_destroyed_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/invalid.py
new file mode 100644
index 0000000000..f32c5f57ca
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/invalid.py
@@ -0,0 +1,15 @@
+import pytest
+import webdriver.bidi.error as error
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("script", [None, False, 42, {}, []])
+async def test_params_script_invalid_type(bidi_session, script):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.script.remove_preload_script(script=script),
+
+
+async def test_params_script_invalid_value(bidi_session):
+ with pytest.raises(error.NoSuchScriptException):
+ await bidi_session.script.remove_preload_script(script="foo"),
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/remove_preload_script.py b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/remove_preload_script.py
new file mode 100644
index 0000000000..2cf35fb9bb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/remove_preload_script.py
@@ -0,0 +1,120 @@
+import pytest
+import webdriver.bidi.error as error
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("type_hint", ["tab", "window"])
+async def test_remove_preload_script(bidi_session, type_hint):
+ script = await bidi_session.script.add_preload_script(
+ function_declaration="() => { window.foo='bar'; }"
+ )
+
+ new_context = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ result = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_context["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "string", "value": "bar"}
+
+ await bidi_session.script.remove_preload_script(script=script)
+
+ new_tab_2 = await bidi_session.browsing_context.create(type_hint=type_hint)
+
+ # Check that changes from preload script were not applied after script was removed
+ result_2 = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab_2["context"]),
+ await_promise=True,
+ )
+ assert result_2 == {"type": "undefined"}
+
+
+@pytest.mark.asyncio
+async def test_remove_preload_script_twice(bidi_session):
+ script = await bidi_session.script.add_preload_script(
+ function_declaration="() => { window.foo='bar'; }"
+ )
+
+ await bidi_session.script.remove_preload_script(script=script)
+
+ # Check that we can not remove the same script twice
+ with pytest.raises(error.NoSuchScriptException):
+ await bidi_session.script.remove_preload_script(script=script)
+
+
+@pytest.mark.asyncio
+async def test_remove_one_of_preload_scripts(bidi_session):
+ script_1 = await bidi_session.script.add_preload_script(
+ function_declaration="() => { window.bar='foo'; }"
+ )
+ script_2 = await bidi_session.script.add_preload_script(
+ function_declaration="() => { window.baz='bar'; }"
+ )
+
+ # Remove one of the scripts
+ await bidi_session.script.remove_preload_script(script=script_1)
+
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+
+ # Check that the first script didn't run
+ result = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "undefined"}
+
+ # Check that the second script still applied the changes to the window
+ result_2 = await bidi_session.script.evaluate(
+ expression="window.baz",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result_2 == {"type": "string", "value": "bar"}
+
+ # Clean up the second script
+ await bidi_session.script.remove_preload_script(script=script_2)
+
+
+@pytest.mark.asyncio
+async def test_remove_script_set_up_for_one_context(
+ bidi_session, add_preload_script, new_tab, test_page, test_page_cross_origin
+):
+ script = await add_preload_script(
+ function_declaration="() => { window.baz = 42; }",
+ contexts=[new_tab["context"]],
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=test_page,
+ wait="complete",
+ )
+
+ # Check that preload script applied the changes to the context
+ result = await bidi_session.script.evaluate(
+ expression="window.baz",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "number", "value": 42}
+
+ await bidi_session.script.remove_preload_script(script=script)
+
+ # Navigate again to see that preload script didn't run
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=test_page_cross_origin,
+ wait="complete",
+ )
+
+ result = await bidi_session.script.evaluate(
+ expression="window.baz",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result == {"type": "undefined"}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/sandbox.py b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/sandbox.py
new file mode 100644
index 0000000000..32befe7f05
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/script/remove_preload_script/sandbox.py
@@ -0,0 +1,42 @@
+import pytest
+
+from webdriver.bidi.modules.script import ContextTarget
+
+
+@pytest.mark.asyncio
+async def test_remove_preload_script_from_sandbox(bidi_session):
+ # Add preload script to make changes in window
+ script_1 = await bidi_session.script.add_preload_script(
+ function_declaration="() => { window.foo = 1; }",
+ )
+ # Add preload script to make changes in sandbox
+ script_2 = await bidi_session.script.add_preload_script(
+ function_declaration="() => { window.bar = 2; }", sandbox="sandbox"
+ )
+
+ # Remove first preload script
+ await bidi_session.script.remove_preload_script(
+ script=script_1,
+ )
+ # Remove second preload script
+ await bidi_session.script.remove_preload_script(
+ script=script_2,
+ )
+
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+
+ # Make sure that changes from first preload script were not applied
+ result_in_window = await bidi_session.script.evaluate(
+ expression="window.foo",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ assert result_in_window == {"type": "undefined"}
+
+ # Make sure that changes from second preload script were not applied
+ result_in_sandbox = await bidi_session.script.evaluate(
+ expression="window.bar",
+ target=ContextTarget(new_tab["context"], "sandbox"),
+ await_promise=True,
+ )
+ assert result_in_sandbox == {"type": "undefined"}
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/session/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/new/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/session/new/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/new/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/new/connect.py b/testing/web-platform/tests/webdriver/tests/bidi/session/new/connect.py
new file mode 100644
index 0000000000..d5872cd3ba
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/new/connect.py
@@ -0,0 +1,38 @@
+import pytest
+import websockets
+
+import webdriver
+
+
+# classic session to enable bidi capability manually
+# Intended to be the first test in this file
+@pytest.mark.asyncio
+@pytest.mark.capabilities({"webSocketUrl": True})
+async def test_websocket_url_connect(session):
+ websocket_url = session.capabilities["webSocketUrl"]
+ async with websockets.connect(websocket_url) as websocket:
+ await websocket.send("Hello world!")
+
+
+# test bidi_session send
+@pytest.mark.asyncio
+async def test_bidi_session_send(bidi_session, send_blocking_command):
+ await send_blocking_command("session.status", {})
+
+
+# bidi session following a bidi session with a different capabilities
+# to test session recreation
+@pytest.mark.asyncio
+@pytest.mark.capabilities({"acceptInsecureCerts": True})
+async def test_bidi_session_with_different_capability(bidi_session,
+ send_blocking_command):
+ await send_blocking_command("session.status", {})
+
+
+# classic session following a bidi session to test session
+# recreation
+# Intended to be the last test in this file to make sure
+# classic session is not impacted by bidi tests
+@pytest.mark.asyncio
+def test_classic_after_bidi_session(session):
+ assert not isinstance(session, webdriver.bidi.BidiSession)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/status/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/session/status/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/status/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/status/status.py b/testing/web-platform/tests/webdriver/tests/bidi/session/status/status.py
new file mode 100644
index 0000000000..13d131bfec
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/status/status.py
@@ -0,0 +1,10 @@
+import pytest
+
+
+# Check that session.status can be used. The actual values for the "ready" and
+# "message" properties are implementation specific.
+@pytest.mark.asyncio
+async def test_bidi_session_status(send_blocking_command):
+ response = await send_blocking_command("session.status", {})
+ assert isinstance(response["ready"], bool)
+ assert isinstance(response["message"], str)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/contexts.py b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/contexts.py
new file mode 100644
index 0000000000..4a8ec1a14e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/contexts.py
@@ -0,0 +1,275 @@
+import pytest
+
+from ... import create_console_api_message, recursive_compare
+
+# The basic use case of subscribing to all contexts for a single event
+# is covered by tests for each event in the dedicated folders.
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_one_context(
+ bidi_session, subscribe_events, top_context, new_tab, wait_for_event, wait_for_future_safe
+):
+ # Subscribe for log events to a specific context
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger console event in the another context
+ await create_console_api_message(bidi_session, new_tab, "text1")
+
+ assert len(events) == 0
+
+ # Trigger another console event in the observed context
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text = await create_console_api_message(bidi_session, top_context, "text2")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+ recursive_compare(
+ {
+ "text": expected_text,
+ },
+ events[0],
+ )
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_one_context_twice(
+ bidi_session, subscribe_events, top_context, wait_for_event, wait_for_future_safe
+):
+ # Subscribe twice for log events to a specific context
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger a console event in the observed context
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text = await create_console_api_message(bidi_session, top_context, "text2")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+ recursive_compare(
+ {
+ "text": expected_text,
+ },
+ events[0],
+ )
+
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_one_context_and_then_to_all(
+ bidi_session, subscribe_events, top_context, new_tab, wait_for_event, wait_for_future_safe
+):
+ # Subscribe for log events to a specific context
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger console event in the another context
+ buffered_event_expected_text = await create_console_api_message(
+ bidi_session, new_tab, "text1"
+ )
+
+ assert len(events) == 0
+
+ # Trigger another console event in the observed context
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text = await create_console_api_message(bidi_session, top_context, "text2")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+ recursive_compare(
+ {
+ "text": expected_text,
+ },
+ events[0],
+ )
+
+ events = []
+
+ # Subscribe to all contexts
+ await subscribe_events(events=["log.entryAdded"])
+
+ # Check that we received the buffered event
+ assert len(events) == 1
+ recursive_compare(
+ {
+ "text": buffered_event_expected_text,
+ },
+ events[0],
+ )
+
+ # Trigger again events in each context
+ expected_text = await create_console_api_message(bidi_session, new_tab, "text3")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 2
+ recursive_compare(
+ {
+ "text": expected_text,
+ },
+ events[1],
+ )
+
+ expected_text = await create_console_api_message(bidi_session, top_context, "text4")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 3
+ recursive_compare(
+ {
+ "text": expected_text,
+ },
+ events[2],
+ )
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_all_context_and_then_to_one_again(
+ bidi_session, subscribe_events, top_context, new_tab, wait_for_event, wait_for_future_safe
+):
+ # Subscribe to all contexts
+ await subscribe_events(events=["log.entryAdded"])
+ # Subscribe to one of the contexts again
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger console event in the context to which we tried to subscribe twice
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message(bidi_session, top_context, "text1")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we received only one event
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_top_context_with_iframes(
+ bidi_session,
+ subscribe_events,
+ wait_for_event, wait_for_future_safe,
+ top_context,
+ test_page_multiple_frames,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_multiple_frames, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts[0]["children"]) == 2
+ frame_1 = contexts[0]["children"][0]
+ frame_2 = contexts[0]["children"][1]
+
+ # Subscribe to the top context
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger console event in the first iframe
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message(bidi_session, frame_1, "text1")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we received the event
+ assert len(events) == 1
+
+ # Trigger console event in the second iframe
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message(bidi_session, frame_2, "text2")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we received the second event as well
+ assert len(events) == 2
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_child_context(
+ bidi_session,
+ subscribe_events,
+ wait_for_event, wait_for_future_safe,
+ top_context,
+ test_page_multiple_frames,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_multiple_frames, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts[0]["children"]) == 2
+ frame_1 = contexts[0]["children"][0]
+ frame_2 = contexts[0]["children"][1]
+
+ # Subscribe to the first frame context
+ await subscribe_events(events=["log.entryAdded"], contexts=[frame_1["context"]])
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger console event in the top context
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message(bidi_session, top_context, "text1")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we received the event
+ assert len(events) == 1
+
+ # Trigger console event in the second iframe
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message(bidi_session, frame_2, "text2")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we received the second event as well
+ assert len(events) == 2
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/events.py b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/events.py
new file mode 100644
index 0000000000..f9d5d6bc21
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/events.py
@@ -0,0 +1,136 @@
+import pytest
+
+# The basic use case of subscribing globally for a single event
+# is covered by tests for each event in the dedicated folders.
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_module(bidi_session, subscribe_events, wait_for_event, wait_for_future_safe):
+ # Subscribe to all browsing context events
+ await subscribe_events(events=["browsingContext"])
+
+ # Track all received browsing context events in the events array
+ events = []
+
+ async def on_event(method, _):
+ events.append(method)
+
+ remove_listener_contextCreated = bidi_session.add_event_listener(
+ "browsingContext.contextCreated", on_event
+ )
+ remove_listener_domContentLoaded = bidi_session.add_event_listener(
+ "browsingContext.domContentLoaded", on_event
+ )
+ remove_listener_load = bidi_session.add_event_listener(
+ "browsingContext.load", on_event
+ )
+
+ # Wait for the last event
+ on_entry_added = wait_for_event("browsingContext.load")
+ await bidi_session.browsing_context.create(type_hint="tab")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 3
+
+ remove_listener_contextCreated()
+ remove_listener_domContentLoaded()
+ remove_listener_load()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_one_event_and_then_to_module(
+ bidi_session, subscribe_events, wait_for_event, wait_for_future_safe
+):
+ # Subscribe to one event
+ await subscribe_events(events=["browsingContext.contextCreated"])
+
+ # Track all received browsing context events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(method)
+
+ remove_listener_contextCreated = bidi_session.add_event_listener(
+ "browsingContext.contextCreated", on_event
+ )
+
+ on_entry_added = wait_for_event("browsingContext.contextCreated")
+ await bidi_session.browsing_context.create(type_hint="tab")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+ assert "browsingContext.contextCreated" in events
+
+ # Subscribe to all browsing context events
+ await subscribe_events(events=["browsingContext"])
+
+ # Clean up the event list
+ events = []
+
+ remove_listener_domContentLoaded = bidi_session.add_event_listener(
+ "browsingContext.domContentLoaded", on_event
+ )
+ remove_listener_load = bidi_session.add_event_listener(
+ "browsingContext.load", on_event
+ )
+
+ # Wait for the last event
+ on_entry_added = wait_for_event("browsingContext.load")
+ await bidi_session.browsing_context.create(type_hint="tab")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we didn't receive duplicates
+ assert len(events) == 3
+
+ remove_listener_contextCreated()
+ remove_listener_domContentLoaded()
+ remove_listener_load()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_module_and_then_to_one_event_again(
+ bidi_session, subscribe_events, wait_for_event, wait_for_future_safe
+):
+ # Subscribe to all browsing context events
+ await subscribe_events(events=["browsingContext"])
+
+ # Track all received browsing context events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(method)
+
+ remove_listener_contextCreated = bidi_session.add_event_listener(
+ "browsingContext.contextCreated", on_event
+ )
+ remove_listener_domContentLoaded = bidi_session.add_event_listener(
+ "browsingContext.domContentLoaded", on_event
+ )
+ remove_listener_load = bidi_session.add_event_listener(
+ "browsingContext.load", on_event
+ )
+
+ # Wait for the last event
+ on_entry_added = wait_for_event("browsingContext.load")
+ await bidi_session.browsing_context.create(type_hint="tab")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 3
+
+ # Subscribe to one event again
+ await subscribe_events(events=["browsingContext.contextCreated"])
+
+ # Clean up the event list
+ events = []
+
+ # Wait for the last event
+ on_entry_added = wait_for_event("browsingContext.load")
+ await bidi_session.browsing_context.create(type_hint="tab")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we didn't receive duplicates
+ assert len(events) == 3
+
+ remove_listener_contextCreated()
+ remove_listener_domContentLoaded()
+ remove_listener_load()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/invalid.py
new file mode 100644
index 0000000000..81c38316f9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/subscribe/invalid.py
@@ -0,0 +1,156 @@
+import asyncio
+
+import pytest
+from webdriver.bidi.error import InvalidArgumentException, NoSuchFrameException
+
+from ... import create_console_api_message
+
+
+@pytest.mark.asyncio
+async def test_params_empty(send_blocking_command):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.subscribe", {})
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [None, True, "foo", 42, {}])
+async def test_params_events_invalid_type(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.subscribe", {"events": value})
+
+
+@pytest.mark.asyncio
+async def test_params_events_empty(bidi_session):
+ response = await bidi_session.session.subscribe(events=[])
+ assert response == {}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_events_value_invalid_type(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.subscribe", {"events": [value]})
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", ["", "foo", "foo.bar", "log.invalidEvent"])
+async def test_params_events_value_invalid_event_name(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.subscribe", {"events": [value]})
+
+
+@pytest.mark.asyncio
+async def test_params_events_value_valid_and_invalid_event_names(
+ bidi_session, send_blocking_command, top_context
+):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.subscribe", {"events": ["log.entryAdded", "some.invalidEvent"]}
+ )
+
+ # Make sure that we didn't subscribe to log.entryAdded because of the error
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ await create_console_api_message(bidi_session, top_context, "text1")
+
+ # Wait for some time before checking the events array
+ await asyncio.sleep(0.5)
+ assert len(events) == 0
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [True, "foo", 42, {}])
+async def test_params_contexts_invalid_type(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.subscribe",
+ {
+ "events": [],
+ "contexts": value,
+ }
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_contexts_empty(bidi_session):
+ response = await bidi_session.session.subscribe(events=[], contexts=[])
+ assert response == {}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_contexts_value_invalid_type(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.subscribe",
+ {
+ "events": [],
+ "contexts": [value],
+ }
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_contexts_value_invalid_value(send_blocking_command):
+ with pytest.raises(NoSuchFrameException):
+ await send_blocking_command(
+ "session.subscribe",
+ {
+ "events": [],
+ "contexts": ["foo"],
+ }
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_contexts_valid_and_invalid_value(
+ bidi_session, send_blocking_command, top_context
+):
+ with pytest.raises(NoSuchFrameException):
+ await send_blocking_command(
+ "session.subscribe",
+ {"events": ["log.entryAdded"], "contexts": [top_context["context"], "foo"]},
+ )
+
+ # Make sure that we didn't subscribe to log.entryAdded because of error
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ await create_console_api_message(bidi_session, top_context, "text1")
+
+ # Wait for some time before checking the events array
+ await asyncio.sleep(0.5)
+ assert len(events) == 0
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_closed_tab(bidi_session, send_blocking_command):
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ # Try to subscribe to the closed context
+ with pytest.raises(NoSuchFrameException):
+ await send_blocking_command(
+ "session.subscribe",
+ {
+ "events": ["log.entryAdded"],
+ "contexts": [new_tab["context"]]
+ },
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/contexts.py b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/contexts.py
new file mode 100644
index 0000000000..c655caa585
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/contexts.py
@@ -0,0 +1,165 @@
+import pytest
+
+from ... import create_console_api_message, recursive_compare
+
+# The basic use case of unsubscribing from all contexts for a single event
+# is covered by tests for each event in the dedicated folders.
+
+
+@pytest.mark.asyncio
+async def test_unsubscribe_from_one_context(
+ bidi_session, top_context, new_tab, wait_for_event, wait_for_future_safe
+):
+ # Subscribe for log events to multiple contexts
+ await bidi_session.session.subscribe(
+ events=["log.entryAdded"], contexts=[top_context["context"], new_tab["context"]]
+ )
+ # Unsubscribe from log events in one of the contexts
+ await bidi_session.session.unsubscribe(
+ events=["log.entryAdded"], contexts=[top_context["context"]]
+ )
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger console event in the unsubscribed context
+ await create_console_api_message(bidi_session, top_context, "text1")
+ assert len(events) == 0
+
+ # Trigger another console event in the still observed context
+ on_entry_added = wait_for_event("log.entryAdded")
+ expected_text = await create_console_api_message(bidi_session, new_tab, "text2")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+ recursive_compare(
+ {
+ "text": expected_text,
+ },
+ events[0],
+ )
+
+ remove_listener()
+ await bidi_session.session.unsubscribe(
+ events=["log.entryAdded"], contexts=[new_tab["context"]]
+ )
+
+
+@pytest.mark.asyncio
+async def test_unsubscribe_from_top_context_with_iframes(
+ bidi_session,
+ top_context,
+ test_page_same_origin_frame,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_same_origin_frame, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+
+ # Subscribe and unsubscribe to the top context
+ await bidi_session.session.subscribe(
+ events=["log.entryAdded"], contexts=[top_context["context"]]
+ )
+ await bidi_session.session.unsubscribe(
+ events=["log.entryAdded"], contexts=[top_context["context"]]
+ )
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger the event in the frame
+ await create_console_api_message(bidi_session, frame, "text1")
+
+ assert len(events) == 0
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_unsubscribe_from_child_context(
+ bidi_session,
+ top_context,
+ test_page_same_origin_frame,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_same_origin_frame, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+
+ assert len(contexts[0]["children"]) == 1
+ frame = contexts[0]["children"][0]
+
+ # Subscribe to top context
+ await bidi_session.session.subscribe(
+ events=["log.entryAdded"], contexts=[top_context["context"]]
+ )
+ # Unsubscribe from the frame context
+ await bidi_session.session.unsubscribe(
+ events=["log.entryAdded"], contexts=[frame["context"]]
+ )
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger the event in the frame
+ await create_console_api_message(bidi_session, frame, "text1")
+ # Trigger the event in the top context
+ await create_console_api_message(bidi_session, top_context, "text2")
+
+ # Make sure we didn't receive any of the triggered events
+ assert len(events) == 0
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_unsubscribe_from_one_context_after_navigation(
+ bidi_session, top_context, test_alt_origin
+):
+ await bidi_session.session.subscribe(
+ events=["log.entryAdded"], contexts=[top_context["context"]]
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_alt_origin, wait="complete"
+ )
+
+ await bidi_session.session.unsubscribe(
+ events=["log.entryAdded"], contexts=[top_context["context"]]
+ )
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ # Trigger the event
+ await create_console_api_message(bidi_session, top_context, "text1")
+
+ # Make sure we successfully unsubscribed
+ assert len(events) == 0
+
+ remove_listener()
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/events.py b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/events.py
new file mode 100644
index 0000000000..a5a0210ac4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/events.py
@@ -0,0 +1,81 @@
+import pytest
+from tests.support.sync import AsyncPoll
+from webdriver.error import TimeoutException
+
+# The basic use case of unsubscribing globally from a single event
+# is covered by tests for each event in the dedicated folders.
+
+
+@pytest.mark.asyncio
+async def test_unsubscribe_from_module(bidi_session):
+ await bidi_session.session.subscribe(events=["browsingContext"])
+ await bidi_session.session.unsubscribe(events=["browsingContext"])
+
+ # Track all received browsing context events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener_contextCreated = bidi_session.add_event_listener(
+ "browsingContext.contextCreated", on_event
+ )
+ remove_listener_domContentLoaded = bidi_session.add_event_listener(
+ "browsingContext.domContentLoaded", on_event
+ )
+ remove_listener_load = bidi_session.add_event_listener(
+ "browsingContext.load", on_event
+ )
+
+ await bidi_session.browsing_context.create(type_hint="tab")
+
+ wait = AsyncPoll(bidi_session, timeout=0.5)
+ with pytest.raises(TimeoutException):
+ await wait.until(lambda _: len(events) > 0)
+
+ remove_listener_contextCreated()
+ remove_listener_domContentLoaded()
+ remove_listener_load()
+
+
+@pytest.mark.asyncio
+async def test_subscribe_to_module_unsubscribe_from_one_event(
+ bidi_session, wait_for_event, wait_for_future_safe
+):
+ await bidi_session.session.subscribe(events=["browsingContext"])
+
+ # Unsubscribe from one event
+ await bidi_session.session.unsubscribe(events=["browsingContext.domContentLoaded"])
+
+ # Track all received browsing context events in the events array
+ events = []
+
+ async def on_event(method, _):
+ events.append(method)
+
+ remove_listener_contextCreated = bidi_session.add_event_listener(
+ "browsingContext.contextCreated", on_event
+ )
+ remove_listener_domContentLoaded = bidi_session.add_event_listener(
+ "browsingContext.domContentLoaded", on_event
+ )
+ remove_listener_load = bidi_session.add_event_listener(
+ "browsingContext.load", on_event
+ )
+
+ # Wait for the last event
+ on_entry_added = wait_for_event("browsingContext.load")
+ await bidi_session.browsing_context.create(type_hint="tab")
+ await wait_for_future_safe(on_entry_added)
+
+ # Make sure we didn't receive browsingContext.domContentLoaded event
+ assert len(events) == 2
+ assert "browsingContext.domContentLoaded" not in events
+
+ remove_listener_contextCreated()
+ remove_listener_domContentLoaded()
+ remove_listener_load()
+
+ # Unsubscribe from the rest of the events
+ await bidi_session.session.unsubscribe(events=["browsingContext.contextCreated"])
+ await bidi_session.session.unsubscribe(events=["browsingContext.load"])
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/invalid.py
new file mode 100644
index 0000000000..5b55556413
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/session/unsubscribe/invalid.py
@@ -0,0 +1,234 @@
+import pytest
+from webdriver.bidi.error import InvalidArgumentException, NoSuchFrameException
+
+from ... import create_console_api_message
+
+
+@pytest.mark.asyncio
+async def test_params_empty(bidi_session, send_blocking_command):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.unsubscribe", {})
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [None, True, "foo", 42, {}])
+async def test_params_events_invalid_type(bidi_session, send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.unsubscribe", {"events": value})
+
+
+@pytest.mark.asyncio
+async def test_params_events_empty(bidi_session):
+ response = await bidi_session.session.unsubscribe(events=[])
+ assert response == {}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_events_value_invalid_type(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.unsubscribe", {"events": [value]})
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", ["", "foo", "foo.bar"])
+async def test_params_events_value_invalid_event_name(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command("session.unsubscribe", {"events": [value]})
+
+
+@pytest.mark.asyncio
+async def test_params_events_value_valid_and_invalid_event_name(
+ bidi_session, subscribe_events, send_blocking_command, wait_for_event, wait_for_future_safe, top_context
+):
+ # Subscribe to a valid event
+ await subscribe_events(events=["log.entryAdded"])
+
+ # Try to unsubscribe from the valid and an invalid event
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe", {"events": ["log.entryAdded", "some.invalidEvent"]}
+ )
+
+ # Make sure that we didn't unsubscribe from log.entryAdded because of the error
+ # and events are still coming
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message(bidi_session, top_context, "text1")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_unsubscribe_from_one_event_and_then_from_module(
+ bidi_session, subscribe_events, send_blocking_command
+):
+ await subscribe_events(events=["browsingContext"])
+
+ # Unsubscribe from one event
+ await bidi_session.session.unsubscribe(events=["browsingContext.domContentLoaded"])
+
+ # Try to unsubscribe from all events
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe", {"events": ["browsingContext"]}
+ )
+
+ # Unsubscribe from the rest of the events
+ await bidi_session.session.unsubscribe(events=["browsingContext.contextCreated"])
+ await bidi_session.session.unsubscribe(events=["browsingContext.load"])
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [True, "foo", 42, {}])
+async def test_params_contexts_invalid_type(bidi_session, send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe",
+ {
+ "events": [],
+ "contexts": value,
+ }
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_contexts_empty(bidi_session):
+ response = await bidi_session.session.unsubscribe(events=[], contexts=[])
+ assert response == {}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_contexts_value_invalid_type(send_blocking_command, value):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe",
+ {
+ "events": [],
+ "contexts": [value],
+ }
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_contexts_value_invalid_value(send_blocking_command):
+ with pytest.raises(NoSuchFrameException):
+ await send_blocking_command(
+ "session.unsubscribe",
+ {
+ "events": [],
+ "contexts": ["foo"],
+ },
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_contexts_value_valid_and_invalid_value(
+ bidi_session, subscribe_events, send_blocking_command, wait_for_event, wait_for_future_safe, top_context
+):
+ # Subscribe to a valid context
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+
+ # Try to unsubscribe from the valid and an invalid context
+ with pytest.raises(NoSuchFrameException):
+ await send_blocking_command(
+ "session.unsubscribe",
+ {"events": ["log.entryAdded"], "contexts": [top_context["context"], "foo"]},
+ )
+
+ # Make sure that we didn't unsubscribe from the valid context because of the error
+ # and events are still coming
+
+ # Track all received log.entryAdded events in the events array
+ events = []
+
+ async def on_event(method, data):
+ events.append(data)
+
+ remove_listener = bidi_session.add_event_listener("log.entryAdded", on_event)
+
+ on_entry_added = wait_for_event("log.entryAdded")
+ await create_console_api_message(bidi_session, top_context, "text1")
+ await wait_for_future_safe(on_entry_added)
+
+ assert len(events) == 1
+
+ remove_listener()
+
+
+@pytest.mark.asyncio
+async def test_unsubscribe_from_closed_tab(
+ bidi_session, subscribe_events, send_blocking_command
+):
+ new_tab = await bidi_session.browsing_context.create(type_hint="tab")
+ # Subscribe to a new context
+ await subscribe_events(events=["log.entryAdded"], contexts=[new_tab["context"]])
+
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+
+ # Try to unsubscribe from the closed context
+ with pytest.raises(NoSuchFrameException):
+ await send_blocking_command(
+ "session.unsubscribe",
+ {"events": ["log.entryAdded"], "contexts": [new_tab["context"]]},
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_unsubscribe_globally_without_subscription(send_blocking_command):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe", {"events": ["log.entryAdded"]}
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_unsubscribe_globally_with_individual_subscription(
+ subscribe_events, send_blocking_command, top_context
+):
+ # Subscribe to one context
+ await subscribe_events(events=["log.entryAdded"], contexts=[top_context["context"]])
+
+ # Try to unsubscribe globally
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe", {"events": ["log.entryAdded"]}
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_unsubscribe_from_one_context_without_subscription(
+ send_blocking_command, top_context
+):
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe",
+ {"events": ["log.entryAdded"], "contexts": [top_context["context"]]},
+ )
+
+
+@pytest.mark.asyncio
+async def test_params_unsubscribe_from_one_context_with_global_subscription(
+ subscribe_events, send_blocking_command, top_context
+):
+ # Subscribe to all contexts
+ await subscribe_events(events=["log.entryAdded"])
+
+ # Try to unsubscribe from one context
+ with pytest.raises(InvalidArgumentException):
+ await send_blocking_command(
+ "session.unsubscribe",
+ {"events": ["log.entryAdded"], "contexts": [top_context["context"]]},
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py
new file mode 100644
index 0000000000..0d7cea96bc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/__init__.py
@@ -0,0 +1,90 @@
+from datetime import datetime, timedelta, timezone
+from typing import Optional
+from webdriver.bidi.modules.network import NetworkBytesValue, NetworkStringValue
+from webdriver.bidi.modules.storage import PartialCookie, PartitionDescriptor
+from .. import any_int, recursive_compare
+
+COOKIE_NAME = 'SOME_COOKIE_NAME'
+COOKIE_VALUE = NetworkStringValue('SOME_COOKIE_VALUE')
+
+
+async def assert_cookie_is_not_set(bidi_session, name: str = COOKIE_NAME):
+ """
+ Asserts the cookie is not set.
+ """
+ all_cookies = await bidi_session.storage.get_cookies()
+ assert 'cookies' in all_cookies
+ assert not any(c for c in all_cookies['cookies'] if c['name'] == name)
+
+
+async def assert_cookie_is_set(
+ bidi_session,
+ domain: str,
+ name: str = COOKIE_NAME,
+ value: str = COOKIE_VALUE,
+ path: str = "/",
+ http_only: bool = False,
+ secure: bool = True,
+ same_site: str = 'none',
+ expiry: Optional[int] = None,
+ partition: Optional[PartitionDescriptor] = None,
+):
+ """
+ Asserts the cookie is set.
+ """
+ all_cookies = await bidi_session.storage.get_cookies(partition=partition)
+ assert 'cookies' in all_cookies
+ actual_cookie = next(c for c in all_cookies['cookies'] if c['name'] == name)
+ expected_cookie = {
+ 'domain': domain,
+ 'httpOnly': http_only,
+ 'name': name,
+ 'path': path,
+ 'sameSite': same_site,
+ 'secure': secure,
+ # Varies depending on the cookie name and value.
+ 'size': any_int,
+ 'value': value,
+ }
+ if expiry is not None:
+ expected_cookie['expiry'] = expiry
+
+ recursive_compare(expected_cookie, actual_cookie)
+
+
+def create_cookie(
+ domain: str,
+ name: str = COOKIE_NAME,
+ value: NetworkBytesValue = COOKIE_VALUE,
+ secure: Optional[bool] = True,
+ path: Optional[str] = None,
+ http_only: Optional[bool] = None,
+ same_site: Optional[str] = None,
+ expiry: Optional[int] = None,
+) -> PartialCookie:
+ """
+ Creates a cookie with the given or default options.
+ """
+ return PartialCookie(
+ domain=domain,
+ name=name,
+ value=value,
+ path=path,
+ http_only=http_only,
+ secure=secure,
+ same_site=same_site,
+ expiry=expiry)
+
+
+def generate_expiry_date(day_diff=1):
+ return (
+ (datetime.utcnow() + timedelta(days=day_diff))
+ .replace(microsecond=0)
+ .replace(tzinfo=timezone.utc)
+ )
+
+
+def format_expiry_string(date):
+ # same formatting as Date.toUTCString() in javascript
+ utc_string_format = "%a, %d %b %Y %H:%M:%S GMT"
+ return date.strftime(utc_string_format)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/conftest.py
new file mode 100644
index 0000000000..0941411fab
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/conftest.py
@@ -0,0 +1,11 @@
+from urllib.parse import urlunsplit
+
+import pytest
+
+
+@pytest.fixture
+def origin(server_config, domain_value):
+ def origin(protocol="https", domain="", subdomain=""):
+ return urlunsplit((protocol, domain_value(domain, subdomain), "", "", ""))
+
+ return origin
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/filter.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/filter.py
new file mode 100644
index 0000000000..b244ef86ac
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/filter.py
@@ -0,0 +1,559 @@
+import pytest
+from webdriver.bidi.modules.network import NetworkBase64Value, NetworkStringValue
+from webdriver.bidi.modules.storage import CookieFilter
+
+from .. import create_cookie, format_expiry_string, generate_expiry_date
+from ... import recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "filter",
+ [
+ {"size": 6},
+ {"value": NetworkStringValue("bar")},
+ {"value": NetworkBase64Value("YmFy")},
+ ],
+)
+async def test_filter(
+ bidi_session, new_tab, test_page, domain_value, add_cookie, filter
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+ value_1 = "bar"
+
+ cookie1_name = "baz"
+ await add_cookie(new_tab["context"], cookie1_name, value_1)
+
+ cookie2_name = "foo"
+ await add_cookie(new_tab["context"], cookie2_name, value_1)
+
+ cookie3_name = "foo_3"
+ await add_cookie(new_tab["context"], cookie3_name, "bar_3")
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter=filter,
+ )
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ # Provide consistent cookies order.
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["name"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": value_1},
+ },
+ cookie_1,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie2_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": value_1},
+ },
+ cookie_2,
+ )
+
+
+async def test_filter_domain(
+ bidi_session,
+ top_context,
+ new_tab,
+ test_page,
+ test_page_cross_origin,
+ domain_value,
+ add_cookie,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page, wait="complete"
+ )
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page_cross_origin, wait="complete"
+ )
+
+ cookie1_name = "bar"
+ cookie1_value = "foo"
+ await add_cookie(top_context["context"], cookie1_name, cookie1_value)
+
+ cookie2_name = "foo"
+ cookie2_value = "bar"
+ await add_cookie(top_context["context"], cookie2_name, cookie2_value)
+
+ cookie3_name = "foo_2"
+ cookie3_value = "bar_2"
+ await add_cookie(new_tab["context"], cookie3_name, cookie3_value)
+ domain = domain_value()
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter=CookieFilter(domain=domain),
+ )
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ # Provide consistent cookies order.
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["name"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ },
+ cookie_1,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie2_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie2_value},
+ },
+ cookie_2,
+ )
+
+
+@pytest.mark.parametrize(
+ "expiry_diff_1, expiry_diff_2",
+ [
+ (1, 2),
+ (1, None),
+ ],
+)
+async def test_filter_expiry(
+ bidi_session,
+ new_tab,
+ test_page,
+ domain_value,
+ add_cookie,
+ expiry_diff_1,
+ expiry_diff_2,
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ cookie1_expiry_date = generate_expiry_date(expiry_diff_1)
+ cookie1_expiry = int(cookie1_expiry_date.timestamp())
+ cookie1_date_string = format_expiry_string(cookie1_expiry_date)
+
+ cookie1_name = "bar"
+ cookie1_value = "foo"
+ await add_cookie(
+ context=new_tab["context"],
+ name=cookie1_name,
+ value=cookie1_value,
+ expiry=cookie1_date_string,
+ )
+
+ cookie2_name = "foo"
+ cookie2_value = "bar"
+ await add_cookie(
+ context=new_tab["context"],
+ name=cookie2_name,
+ value=cookie2_value,
+ expiry=cookie1_date_string,
+ )
+
+ cookie3_name = "foo_3"
+ if expiry_diff_2 is None:
+ cookie2_date_string = None
+ else:
+ cookie2_expiry_date = generate_expiry_date(expiry_diff_2)
+ cookie2_date_string = format_expiry_string(cookie2_expiry_date)
+
+ await add_cookie(
+ new_tab["context"], cookie3_name, "bar_3", expiry=cookie2_date_string
+ )
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter=CookieFilter(expiry=cookie1_expiry),
+ )
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ # Provide consistent cookies order.
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["name"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "expiry": cookie1_expiry,
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ },
+ cookie_1,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "expiry": cookie1_expiry,
+ "httpOnly": False,
+ "name": cookie2_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie2_value},
+ },
+ cookie_2,
+ )
+
+
+async def test_filter_name(bidi_session, new_tab, test_page, domain_value, add_cookie):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ cookie1_name = "foo"
+ cookie1_value = "bar"
+ await add_cookie(new_tab["context"], cookie1_name, cookie1_value)
+
+ cookie2_name = "foo_2"
+ await add_cookie(new_tab["context"], cookie2_name, "bar_2")
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter={"name": "foo"},
+ )
+
+ recursive_compare(
+ {
+ "cookies": [
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ }
+ ],
+ "partitionKey": {},
+ },
+ cookies,
+ )
+
+
+@pytest.mark.parametrize(
+ "same_site_1, same_site_2",
+ [
+ ("none", "strict"),
+ ("lax", "none"),
+ ("strict", "none"),
+ ("lax", "strict"),
+ ("strict", "lax"),
+ ],
+)
+async def test_filter_same_site(
+ bidi_session, new_tab, test_page, domain_value, same_site_1, same_site_2, add_cookie
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ cookie1_name = "bar"
+ cookie1_value = "foo"
+ await add_cookie(
+ new_tab["context"],
+ cookie1_name,
+ cookie1_value,
+ same_site=same_site_1,
+ )
+
+ cookie2_name = "foo"
+ cookie2_value = "bar"
+ await add_cookie(
+ new_tab["context"],
+ cookie2_name,
+ cookie2_value,
+ same_site=same_site_1,
+ )
+
+ cookie3_name = "foo_3"
+ await add_cookie(new_tab["context"], cookie3_name, "bar_3", same_site=same_site_2)
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter=CookieFilter(same_site=same_site_1),
+ )
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ # Provide consistent cookies order.
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["name"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": same_site_1,
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ },
+ cookie_1,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie2_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": same_site_1,
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie2_value},
+ },
+ cookie_2,
+ )
+
+
+@pytest.mark.parametrize(
+ "secure_1, secure_2",
+ [(True, False), (False, True)],
+)
+async def test_filter_secure(
+ bidi_session, new_tab, test_page, domain_value, add_cookie, secure_1, secure_2
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ cookie1_name = "bar"
+ cookie1_value = "foo"
+ await add_cookie(
+ new_tab["context"],
+ cookie1_name,
+ cookie1_value,
+ secure=secure_1,
+ )
+
+ cookie2_name = "foo"
+ cookie2_value = "bar"
+ await add_cookie(
+ new_tab["context"],
+ cookie2_name,
+ cookie2_value,
+ secure=secure_1,
+ )
+
+ cookie3_name = "foo_3"
+ await add_cookie(new_tab["context"], cookie3_name, "bar_3", secure=secure_2)
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter=CookieFilter(secure=secure_1),
+ )
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ # Provide consistent cookies order.
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["name"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": secure_1,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ },
+ cookie_1,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie2_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": secure_1,
+ "size": 6,
+ "value": {"type": "string", "value": cookie2_value},
+ },
+ cookie_2,
+ )
+
+
+@pytest.mark.parametrize(
+ "path_1, path_2",
+ [
+ ("/webdriver/tests/support", "/"),
+ ("/", None),
+ ("/webdriver", "/webdriver/tests"),
+ ],
+)
+async def test_filter_path(
+ bidi_session,
+ new_tab,
+ test_page,
+ domain_value,
+ add_cookie,
+ path_1,
+ path_2,
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ cookie1_name = "bar"
+ cookie1_value = "foo"
+ await add_cookie(
+ new_tab["context"],
+ cookie1_name,
+ cookie1_value,
+ path=path_1,
+ )
+
+ cookie2_name = "foo"
+ cookie2_value = "bar"
+ await add_cookie(
+ new_tab["context"],
+ cookie2_name,
+ cookie2_value,
+ path=path_1,
+ )
+
+ cookie3_name = "foo_3"
+ await add_cookie(new_tab["context"], cookie3_name, "bar_3", path=path_2)
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter=CookieFilter(path=path_1),
+ )
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["name"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": path_1,
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ },
+ cookie_1,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie2_name,
+ "path": path_1,
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie2_value},
+ },
+ cookie_2,
+ )
+
+
+@pytest.mark.parametrize(
+ "http_only_1, http_only_2",
+ [(True, False), (False, True)],
+)
+async def test_filter_http_only(
+ bidi_session, new_tab, test_page, domain_value, set_cookie, http_only_1, http_only_2
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ cookie1_name = "bar"
+ cookie1_value = "foo"
+ await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(),
+ name=cookie1_name,
+ value=NetworkStringValue(cookie1_value),
+ http_only=http_only_1,
+ )
+ )
+
+ cookie2_name = "foo"
+ cookie2_value = "bar"
+ await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(),
+ name=cookie2_name,
+ value=NetworkStringValue(cookie2_value),
+ http_only=http_only_1,
+ )
+ )
+
+ cookie3_name = "foo_2"
+ cookie3_value = "bar_2"
+ await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(),
+ name=cookie3_name,
+ value=NetworkStringValue(cookie3_value),
+ http_only=http_only_2,
+ )
+ )
+
+ cookies = await bidi_session.storage.get_cookies(
+ filter=CookieFilter(http_only=http_only_1),
+ )
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["name"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": http_only_1,
+ "name": cookie1_name,
+ "path": "/",
+ "sameSite": "none",
+ "secure": True,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ },
+ cookie_1,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": http_only_1,
+ "name": cookie2_name,
+ "path": "/",
+ "sameSite": "none",
+ "secure": True,
+ "size": 6,
+ "value": {"type": "string", "value": cookie2_value},
+ },
+ cookie_2,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/invalid.py
new file mode 100644
index 0000000000..fbd5647f30
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/invalid.py
@@ -0,0 +1,155 @@
+import pytest
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.network import NetworkBase64Value, NetworkStringValue
+from webdriver.bidi.modules.storage import (
+ BrowsingContextPartitionDescriptor,
+ CookieFilter,
+ StorageKeyPartitionDescriptor,
+)
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_filter_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=value)
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_filter_domain_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(domain=value))
+
+
+@pytest.mark.parametrize("value", [False, "foo", {}, [], -1, 0.5])
+async def test_params_filter_expiry_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(expiry=value))
+
+
+@pytest.mark.parametrize("value", ["foo", {}, [], 42])
+async def test_params_filter_http_only_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(http_only=value))
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_filter_name_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(name=value))
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_filter_path_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(path=value))
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_filter_same_site_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(same_site=value))
+
+
+@pytest.mark.parametrize("value", ["", "INVALID_SAME_SITE_STATE"])
+async def test_params_filter_same_site_invalid_value(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(same_site=value))
+
+
+@pytest.mark.parametrize("value", ["foo", {}, [], 42])
+async def test_params_filter_secure_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(secure=value))
+
+
+@pytest.mark.parametrize("value", [False, "foo", {}, [], -1, 0.5])
+async def test_params_filter_size_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(size=value))
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_filter_value_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(filter=CookieFilter(value=value))
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_filter_value_type_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(
+ filter=CookieFilter(value={"type": value})
+ )
+
+
+async def test_params_filter_value_type_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(
+ filter=CookieFilter(value={"type": "foo"})
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_filter_value_base64_type_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(
+ filter=CookieFilter(value=NetworkBase64Value(value))
+ )
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_filter_value_string_type_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(
+ filter=CookieFilter(value=NetworkStringValue(value))
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, "foo", []])
+async def test_params_partition_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(partition=value)
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_partition_type_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(partition={"type": value})
+
+
+async def test_params_partition_type_invalid_value(bidi_session):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(partition={"type": "foo"})
+
+
+@pytest.mark.parametrize("value", [None, False, 42, {}, []])
+async def test_params_partition_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(
+ partition=BrowsingContextPartitionDescriptor(context=value)
+ )
+
+
+async def test_partition_invalid_context(bidi_session):
+ with pytest.raises(error.NoSuchFrameException):
+ await bidi_session.storage.get_cookies(
+ partition=BrowsingContextPartitionDescriptor("foo")
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_partition_source_origin_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(
+ partition=StorageKeyPartitionDescriptor(source_origin=value)
+ )
+
+
+@pytest.mark.parametrize("value", [False, 42, {}, []])
+async def test_params_partition_user_context_invalid_type(bidi_session, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await bidi_session.storage.get_cookies(
+ partition=StorageKeyPartitionDescriptor(user_context=value)
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/partition.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/partition.py
new file mode 100644
index 0000000000..a1c2650352
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/get_cookies/partition.py
@@ -0,0 +1,258 @@
+import pytest
+
+from webdriver.bidi.modules.network import NetworkStringValue
+from webdriver.bidi.modules.storage import (
+ BrowsingContextPartitionDescriptor,
+ StorageKeyPartitionDescriptor,
+)
+
+from .. import create_cookie
+from ... import recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_default_partition(
+ bidi_session,
+ top_context,
+ new_tab,
+ test_page,
+ test_page_cross_origin,
+ domain_value,
+ add_cookie,
+):
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_cross_origin, wait="complete"
+ )
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ cookie1_name = "foo"
+ cookie1_value = "bar"
+ await add_cookie(new_tab["context"], cookie1_name, cookie1_value)
+
+ cookie2_name = "foo_2"
+ cookie2_value = "bar_2"
+ await add_cookie(top_context["context"], cookie2_name, cookie2_value)
+
+ cookies = await bidi_session.storage.get_cookies()
+
+ assert cookies["partitionKey"] == {}
+ assert len(cookies["cookies"]) == 2
+ # Provide consistent cookies order.
+ (cookie_1, cookie_2) = sorted(cookies["cookies"], key=lambda c: c["domain"])
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie1_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie1_value},
+ },
+ cookie_2,
+ )
+ recursive_compare(
+ {
+ "domain": domain_value("alt"),
+ "httpOnly": False,
+ "name": cookie2_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 10,
+ "value": {"type": "string", "value": cookie2_value},
+ },
+ cookie_1,
+ )
+
+
+async def test_partition_context(
+ bidi_session,
+ new_tab,
+ test_page,
+ domain_value,
+ add_cookie,
+ create_user_context,
+ test_page_cross_origin,
+):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=test_page, wait="complete"
+ )
+
+ user_context = await create_user_context()
+ # Create a new browsing context in another user context.
+ new_context = await bidi_session.browsing_context.create(
+ user_context=user_context, type_hint="tab"
+ )
+ await bidi_session.browsing_context.navigate(
+ context=new_context["context"], url=test_page_cross_origin, wait="complete"
+ )
+
+ cookie_name = "foo"
+ cookie_value = "bar"
+ await add_cookie(new_tab["context"], cookie_name, cookie_value)
+
+ # Check that added cookies are present on the right context.
+ cookies = await bidi_session.storage.get_cookies(
+ partition=BrowsingContextPartitionDescriptor(new_tab["context"])
+ )
+
+ # `partitionKey` here might contain `sourceOrigin` for certain browser implementation,
+ # so use `recursive_compare` to allow additional fields to be present.
+ recursive_compare({"partitionKey": {}}, cookies)
+
+ assert len(cookies["cookies"]) == 1
+ recursive_compare(
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie_name,
+ "path": "/webdriver/tests/support",
+ "sameSite": "none",
+ "secure": False,
+ "size": 6,
+ "value": {"type": "string", "value": cookie_value},
+ },
+ cookies["cookies"][0],
+ )
+
+ # Check that added cookies are not present on the context in the other user context.
+ cookies = await bidi_session.storage.get_cookies(
+ partition=BrowsingContextPartitionDescriptor(new_context["context"])
+ )
+
+ # `partitionKey` here might contain `sourceOrigin` for certain browser implementation,
+ # so use `recursive_compare` to allow additional fields to be present.
+ recursive_compare({"partitionKey": {}}, cookies)
+ assert len(cookies["cookies"]) == 0
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+async def test_partition_context_iframe(
+ bidi_session, new_tab, inline, domain_value, domain, set_cookie
+):
+ iframe_url = inline("<div id='in-iframe'>foo</div>", domain=domain)
+ page_url = inline(f"<iframe src='{iframe_url}'></iframe>")
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=page_url, wait="complete"
+ )
+
+ contexts = await bidi_session.browsing_context.get_tree(root=new_tab["context"])
+ iframe_context = contexts[0]["children"][0]
+
+ cookie_name = "foo"
+ cookie_value = "bar"
+ await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(domain),
+ name=cookie_name,
+ value=NetworkStringValue(cookie_value),
+ ),
+ partition=BrowsingContextPartitionDescriptor(iframe_context["context"]),
+ )
+
+ # Check that added cookies are present on the right context
+ cookies = await bidi_session.storage.get_cookies(
+ partition=BrowsingContextPartitionDescriptor(iframe_context["context"])
+ )
+
+ recursive_compare(
+ {
+ "cookies": [
+ {
+ "domain": domain_value(domain=domain),
+ "httpOnly": False,
+ "name": cookie_name,
+ "path": "/",
+ "sameSite": "none",
+ "secure": True,
+ "size": 6,
+ "value": {"type": "string", "value": cookie_value},
+ }
+ ],
+ "partitionKey": {},
+ },
+ cookies,
+ )
+
+
+@pytest.mark.parametrize(
+ "protocol",
+ [
+ "http",
+ "https",
+ ],
+)
+async def test_partition_source_origin(
+ bidi_session,
+ new_tab,
+ top_context,
+ inline,
+ test_page_cross_origin,
+ domain_value,
+ origin,
+ set_cookie,
+ protocol,
+):
+ url = inline("<div>bar</div>", protocol=protocol)
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"], url=url, wait="complete"
+ )
+ source_origin_1 = origin(protocol)
+
+ cookie_name = "foo"
+ cookie_value = "bar"
+ await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(),
+ name=cookie_name,
+ value=NetworkStringValue(cookie_value),
+ ),
+ partition=StorageKeyPartitionDescriptor(source_origin=source_origin_1),
+ )
+
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"], url=test_page_cross_origin, wait="complete"
+ )
+ source_origin_2 = origin(domain="alt")
+
+ # Check that added cookies are present on the right origin
+ cookies = await bidi_session.storage.get_cookies(
+ partition=StorageKeyPartitionDescriptor(source_origin=source_origin_1)
+ )
+
+ recursive_compare(
+ {
+ "cookies": [
+ {
+ "domain": domain_value(),
+ "httpOnly": False,
+ "name": cookie_name,
+ "path": "/",
+ "sameSite": "none",
+ "secure": True,
+ "size": 6,
+ "value": {"type": "string", "value": cookie_value},
+ }
+ ],
+ "partitionKey": {"sourceOrigin": source_origin_1},
+ },
+ cookies,
+ )
+
+ # Check that added cookies are present on the other origin.
+ cookies = await bidi_session.storage.get_cookies(
+ partition=StorageKeyPartitionDescriptor(source_origin=source_origin_2)
+ )
+
+ recursive_compare(
+ {
+ "cookies": [],
+ "partitionKey": {"sourceOrigin": source_origin_2},
+ },
+ cookies,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/__init__.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_domain.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_domain.py
new file mode 100644
index 0000000000..558d49c186
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_domain.py
@@ -0,0 +1,19 @@
+import pytest
+from .. import assert_cookie_is_set, create_cookie
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "domain_key, subdomain_key",
+ [
+ ("", ""),
+ ("", "www"),
+ ("alt", ""),
+ ("alt", "www"),
+ ])
+async def test_cookie_domain(bidi_session, set_cookie, test_page, domain_value, domain_key, subdomain_key):
+ domain = domain_value(domain_key, subdomain_key)
+
+ await set_cookie(cookie=create_cookie(domain=domain))
+ await assert_cookie_is_set(bidi_session, domain=domain)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_expiry.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_expiry.py
new file mode 100644
index 0000000000..4e49479a87
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_expiry.py
@@ -0,0 +1,51 @@
+import pytest
+from .. import assert_cookie_is_not_set, assert_cookie_is_set, create_cookie
+from datetime import datetime, timedelta
+import time
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_cookie_expiry_unset(bidi_session, set_cookie, test_page, domain_value):
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(),
+ expiry=None))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ await assert_cookie_is_set(bidi_session, expiry=None, domain=domain_value())
+
+
+async def test_cookie_expiry_future(bidi_session, set_cookie, test_page, domain_value):
+ tomorrow = datetime.now() + timedelta(1)
+ tomorrow_timestamp = time.mktime(tomorrow.timetuple())
+
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(),
+ expiry=tomorrow_timestamp))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ await assert_cookie_is_set(bidi_session, expiry=tomorrow_timestamp, domain=domain_value())
+
+
+async def test_cookie_expiry_past(bidi_session, set_cookie, test_page, domain_value):
+ yesterday = datetime.now() - timedelta(1)
+ yesterday_timestamp = time.mktime(yesterday.timetuple())
+
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(
+ domain=domain_value(),
+ expiry=yesterday_timestamp))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ await assert_cookie_is_not_set(bidi_session)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_http_only.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_http_only.py
new file mode 100644
index 0000000000..4473fbf576
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_http_only.py
@@ -0,0 +1,29 @@
+import pytest
+from .. import assert_cookie_is_set, create_cookie
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "http_only",
+ [
+ True,
+ False,
+ None
+ ])
+async def test_cookie_http_only(bidi_session, set_cookie, test_page, domain_value, http_only):
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(domain=domain_value(), http_only=http_only))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ # `httpOnly` defaults to `false`.
+ expected_http_only = http_only if http_only is not None else False
+
+ await assert_cookie_is_set(
+ bidi_session,
+ domain=domain_value(),
+ http_only=expected_http_only,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_name.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_name.py
new file mode 100644
index 0000000000..0aed1bb6b9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_name.py
@@ -0,0 +1,16 @@
+import pytest
+from .. import assert_cookie_is_set, create_cookie
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "name",
+ [
+ "",
+ "cookie name with special symbols !@#$%&*()_+-{}[]|\\:\"'<>,.?/`~",
+ "123cookie",
+ ])
+async def test_cookie_name(bidi_session, set_cookie, test_page, domain_value, name):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), name=name))
+ await assert_cookie_is_set(bidi_session, name=name, domain=domain_value())
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_path.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_path.py
new file mode 100644
index 0000000000..727d24348a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_path.py
@@ -0,0 +1,25 @@
+import pytest
+from .. import assert_cookie_is_set, create_cookie
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/",
+ "/some_path",
+ "/some/nested/path",
+ None
+ ]
+)
+async def test_cookie_path(bidi_session, test_page, set_cookie, domain_value, path):
+ set_cookie_result = await set_cookie(cookie=create_cookie(domain=domain_value(), path=path))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ # `path` defaults to "/".
+ expected_path = path if path is not None else "/"
+ await assert_cookie_is_set(bidi_session, path=expected_path, domain=domain_value())
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_same_site.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_same_site.py
new file mode 100644
index 0000000000..dfc94c5727
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_same_site.py
@@ -0,0 +1,26 @@
+import pytest
+from .. import assert_cookie_is_set, create_cookie
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "same_site",
+ [
+ "strict",
+ "lax",
+ "none",
+ None
+ ]
+)
+async def test_cookie_secure(bidi_session, set_cookie, test_page, domain_value, same_site):
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(domain=domain_value(), same_site=same_site))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ # `same_site` defaults to "none".
+ expected_same_site = same_site if same_site is not None else 'none'
+ await assert_cookie_is_set(bidi_session, domain=domain_value(), same_site=expected_same_site)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_secure.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_secure.py
new file mode 100644
index 0000000000..ef1060cb46
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_secure.py
@@ -0,0 +1,25 @@
+import pytest
+from .. import assert_cookie_is_set, create_cookie
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "secure",
+ [
+ True,
+ False,
+ None
+ ]
+)
+async def test_cookie_secure(bidi_session, set_cookie, test_page, domain_value, secure):
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(domain=domain_value(), secure=secure))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ # `secure` defaults to `false`.
+ expected_secure = secure if secure is not None else False
+ await assert_cookie_is_set(bidi_session, domain=domain_value(), secure=expected_secure)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_value.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_value.py
new file mode 100644
index 0000000000..a5a2b082d5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/cookie_value.py
@@ -0,0 +1,20 @@
+import pytest
+from .. import assert_cookie_is_set, create_cookie
+from webdriver.bidi.modules.network import NetworkStringValue
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "str_value",
+ [
+ "simple_value",
+ "special_symbols =!@#$%^&*()_+-{}[]|\\:\"'<>,.?/`~"
+ ])
+async def test_cookie_value_string(bidi_session, set_cookie, test_page, domain_value, str_value):
+ value = NetworkStringValue(str_value)
+
+ await set_cookie(cookie=create_cookie(domain=domain_value(), value=value))
+ await assert_cookie_is_set(bidi_session, value=value, domain=domain_value())
+
+# TODO: test `test_cookie_value_base64`.
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/invalid.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/invalid.py
new file mode 100644
index 0000000000..53d2573575
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/invalid.py
@@ -0,0 +1,126 @@
+import pytest
+from .. import create_cookie
+import webdriver.bidi.error as error
+from webdriver.bidi.modules.network import NetworkBase64Value, NetworkStringValue
+from webdriver.bidi.modules.storage import BrowsingContextPartitionDescriptor, StorageKeyPartitionDescriptor
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize("cookie", [None, False, 42, "foo", []])
+async def test_cookie_invalid_type(set_cookie, cookie):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=cookie)
+
+
+@pytest.mark.parametrize("domain", [None, False, 42, {}, []])
+async def test_cookie_domain_invalid_type(set_cookie, test_page, domain):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain))
+
+
+@pytest.mark.parametrize("expiry", [False, "SOME_STRING_VALUE", {}, []])
+async def test_cookie_expiry_invalid_type(set_cookie, test_page, domain_value, expiry):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), expiry=expiry))
+
+
+@pytest.mark.parametrize("http_only", [42, "SOME_STRING_VALUE", {}, []])
+async def test_cookie_http_only_invalid_type(set_cookie, test_page, domain_value, http_only):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), http_only=http_only))
+
+
+@pytest.mark.parametrize("name", [None, False, 42, {}, []])
+async def test_cookie_name_invalid_type(set_cookie, test_page, domain_value, name):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), name=name))
+
+
+@pytest.mark.parametrize("path", [False, 42, {}, []])
+async def test_cookie_path_invalid_type(set_cookie, test_page, domain_value, path):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(
+ cookie=create_cookie(domain=domain_value(), path=path))
+
+
+@pytest.mark.parametrize("same_site", ["", "INVALID_SAME_SITE_STATE"])
+async def test_cookie_same_site_invalid_value(set_cookie, test_page, domain_value, same_site):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), same_site=same_site))
+
+
+@pytest.mark.parametrize("same_site", [42, False, {}, []])
+async def test_cookie_same_site_invalid_type(set_cookie, test_page, domain_value, same_site):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), same_site=same_site))
+
+
+@pytest.mark.parametrize("secure", [42, "SOME_STRING_VALUE", {}, []])
+async def test_cookie_secure_invalid_type(set_cookie, test_page, domain_value, secure):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), secure=secure))
+
+
+@pytest.mark.parametrize("value", [None, False, 42, "SOME_STRING_VALUE", {}, {"type": "SOME_INVALID_TYPE"}, []])
+async def test_cookie_value_invalid_type(set_cookie, test_page, domain_value, value):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), value=value))
+
+
+@pytest.mark.parametrize("str_value", [None, False, 42, {}, []])
+async def test_cookie_value_string_invalid_type(set_cookie, test_page, domain_value, str_value):
+ value = NetworkStringValue(str_value)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), value=value))
+
+
+@pytest.mark.parametrize("base64", [None, False, 42, {}, []])
+async def test_cookie_value_base64_invalid_type(set_cookie, domain_value, base64):
+ value = NetworkBase64Value(base64)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value(), value=value))
+
+
+@pytest.mark.parametrize("partition", [42, False, "SOME_STRING_VALUE", {}, {"type": "SOME_INVALID_TYPE"}, []])
+async def test_partition_invalid_type(set_cookie, test_page, domain_value, partition):
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value()), partition=partition)
+
+
+@pytest.mark.parametrize("browsing_context", [None, 42, False, {}, []])
+async def test_partition_context_invalid_type(set_cookie, test_page, origin, domain_value, browsing_context):
+ partition = BrowsingContextPartitionDescriptor(browsing_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value()), partition=partition)
+
+
+async def test_partition_context_unknown(set_cookie, test_page, origin, domain_value):
+ partition = BrowsingContextPartitionDescriptor("UNKNOWN_CONTEXT")
+
+ with pytest.raises(error.NoSuchFrameException):
+ await set_cookie(cookie=create_cookie(domain=domain_value()), partition=partition)
+
+
+@pytest.mark.parametrize("source_origin", [42, False, {}, []])
+async def test_partition_storage_key_source_origin_invalid_type(set_cookie, test_page, origin, domain_value,
+ source_origin):
+ partition = StorageKeyPartitionDescriptor(source_origin=source_origin)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value()), partition=partition)
+
+
+@pytest.mark.parametrize("user_context", [42, False, {}, []])
+async def test_partition_storage_key_user_context_invalid_type(set_cookie, test_page, origin, domain_value,
+ user_context):
+ partition = StorageKeyPartitionDescriptor(user_context=user_context)
+
+ with pytest.raises(error.InvalidArgumentException):
+ await set_cookie(cookie=create_cookie(domain=domain_value()), partition=partition)
+
+# TODO: test `test_partition_storage_key_user_context_unknown`.
+# TODO: test `test_partition_storage_key_user_context_invalid_type`.
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/page_protocols.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/page_protocols.py
new file mode 100644
index 0000000000..4cb712b372
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/page_protocols.py
@@ -0,0 +1,25 @@
+import pytest
+from urllib.parse import urlparse
+from .. import assert_cookie_is_set, create_cookie
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+ "protocol",
+ [
+ "http",
+ "https",
+ ]
+)
+async def test_page_protocols(bidi_session, set_cookie, get_test_page, protocol):
+ url = get_test_page(protocol=protocol)
+ domain = urlparse(url).hostname
+ set_cookie_result = await set_cookie(cookie=create_cookie(domain=domain))
+
+ assert set_cookie_result == {
+ 'partitionKey': {},
+ }
+
+ # Assert the cookie is actually set.
+ await assert_cookie_is_set(bidi_session, domain=domain)
diff --git a/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/partition.py b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/partition.py
new file mode 100644
index 0000000000..cb0ace1f40
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/bidi/storage/set_cookie/partition.py
@@ -0,0 +1,78 @@
+import pytest
+from webdriver.bidi.modules.storage import BrowsingContextPartitionDescriptor, StorageKeyPartitionDescriptor
+from .. import assert_cookie_is_set, create_cookie
+from ... import recursive_compare
+
+pytestmark = pytest.mark.asyncio
+
+
+def assert_set_cookie_result(set_cookie_result, partition):
+ """
+ Asserts the result of `set_cookie` command depending on the partition type.
+ """
+ if isinstance(partition, BrowsingContextPartitionDescriptor):
+ # Browsing context does not require a `sourceOrigin` partition key, but it can be present depending on the
+ # browser implementation.
+ # `recursive_compare` allows the actual result to be any extension of the expected one.
+ recursive_compare({'partitionKey': {}, }, set_cookie_result)
+ return
+ if isinstance(partition, StorageKeyPartitionDescriptor):
+ expected_partition_key = {}
+ if "sourceOrigin" in partition:
+ # `sourceOrigin` should be in the result, as it was used for setting cookie.
+ expected_partition_key["sourceOrigin"] = partition["sourceOrigin"]
+ # The specific partition keys can contain other browser-specific keys.
+ # `recursive_compare` allows the actual result to be any extension of the expected one.
+ recursive_compare({'partitionKey': expected_partition_key}, set_cookie_result)
+ return
+ assert False, f"Unsupported partition type {type(partition)}."
+
+
+async def test_partition_context(bidi_session, set_cookie, top_context, test_page, domain_value):
+ await bidi_session.browsing_context.navigate(context=top_context["context"], url=test_page, wait="complete")
+
+ partition = BrowsingContextPartitionDescriptor(top_context["context"])
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(domain=domain_value()),
+ partition=partition)
+ assert_set_cookie_result(set_cookie_result, partition)
+
+ await assert_cookie_is_set(bidi_session, domain=domain_value())
+
+
+async def test_partition_context_frame(bidi_session, set_cookie, top_context, test_page, domain_value, inline):
+ frame_url = inline("<div>bar</div>", domain="alt")
+ root_page_url = inline(f"<iframe src='{frame_url}'></iframe>")
+ root_page_domain = domain_value()
+
+ # Navigate to a page with a frame.
+ await bidi_session.browsing_context.navigate(
+ context=top_context["context"],
+ url=root_page_url,
+ wait="complete",
+ )
+
+ all_contexts = await bidi_session.browsing_context.get_tree(root=top_context["context"])
+ frame_context_id = all_contexts[0]["children"][0]["context"]
+
+ partition = BrowsingContextPartitionDescriptor(frame_context_id)
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(domain=root_page_domain),
+ partition=partition)
+ assert_set_cookie_result(set_cookie_result, partition)
+
+ await assert_cookie_is_set(bidi_session, domain=root_page_domain)
+
+
+async def test_partition_storage_key_source_origin(bidi_session, set_cookie, test_page, origin, domain_value):
+ source_origin = origin()
+ partition = StorageKeyPartitionDescriptor(source_origin=source_origin)
+
+ set_cookie_result = await set_cookie(
+ cookie=create_cookie(domain=domain_value()),
+ partition=partition)
+ assert_set_cookie_result(set_cookie_result, partition)
+
+ await assert_cookie_is_set(bidi_session, domain=domain_value(), partition=partition)
+
+# TODO: test `test_partition_storage_key_user_context`.
diff --git a/testing/web-platform/tests/webdriver/tests/classic/accept_alert/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/accept_alert/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/accept_alert/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/accept_alert/accept.py b/testing/web-platform/tests/webdriver/tests/classic/accept_alert/accept.py
new file mode 100644
index 0000000000..b83477e5ca
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/accept_alert/accept.py
@@ -0,0 +1,110 @@
+import pytest
+
+from webdriver.error import NoSuchAlertException
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import wait_for_new_handle
+from tests.support.sync import Poll
+
+
+def accept_alert(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/alert/accept".format(**vars(session)))
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<script>window.alert('Hello');</script>")
+
+ response = accept_alert(session)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_level_browsing_context(session, closed_window):
+ response = accept_alert(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = accept_alert(session)
+ assert_error(response, "no such alert")
+
+
+def test_no_user_prompt(session):
+ response = accept_alert(session)
+ assert_error(response, "no such alert")
+
+
+def test_accept_alert(session, inline):
+ session.url = inline("<script>window.alert('Hello');</script>")
+
+ response = accept_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+
+def test_accept_confirm(session, inline):
+ session.url = inline("<script>window.result = window.confirm('Hello');</script>")
+
+ response = accept_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+ assert session.execute_script("return window.result") is True
+
+
+def test_accept_prompt(session, inline):
+ session.url = inline("""
+ <script>
+ window.result = window.prompt('Enter Your Name: ', 'Federer');
+ </script>
+ """)
+
+ response = accept_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+ assert session.execute_script("return window.result") == "Federer"
+
+
+def test_unexpected_alert(session):
+ session.execute_script("window.setTimeout(function() { window.alert('Hello'); }, 100);")
+ wait = Poll(
+ session,
+ timeout=5,
+ ignored_exceptions=NoSuchAlertException,
+ message="No user prompt with text 'Hello' detected")
+ wait.until(lambda s: s.alert.text == "Hello")
+
+ response = accept_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+
+def test_accept_in_popup_window(session, inline):
+ orig_handles = session.handles
+
+ session.url = inline("""
+ <button onclick="window.open('about:blank', '_blank', 'width=500; height=200;resizable=yes');">open</button>
+ """)
+ button = session.find.css("button", all=False)
+ button.click()
+
+ session.window_handle = wait_for_new_handle(session, orig_handles)
+ session.url = inline("""
+ <script>window.alert("Hello")</script>
+ """)
+
+ response = accept_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
diff --git a/testing/web-platform/tests/webdriver/tests/classic/add_cookie/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py
new file mode 100644
index 0000000000..24b71c52fd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/add.py
@@ -0,0 +1,288 @@
+import pytest
+
+from datetime import datetime, timedelta
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import clear_all_cookies
+
+
+def add_cookie(session, cookie):
+ return session.transport.send(
+ "POST", "session/{session_id}/cookie".format(**vars(session)),
+ {"cookie": cookie})
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/cookie".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_null_response_value(session, url):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ response = add_cookie(session, new_cookie)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ }
+
+ response = add_cookie(session, new_cookie)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ }
+
+ response = add_cookie(session, new_cookie)
+ assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize(
+ "page",
+ [
+ "about:blank",
+ "blob:foo/bar",
+ "data:text/html;charset=utf-8,<p>foo</p>",
+ "file:///foo/bar",
+ "ftp://example.org",
+ "javascript:foo",
+ "ws://example.org",
+ "wss://example.org",
+ ],
+ ids=[
+ "about",
+ "blob",
+ "data",
+ "file",
+ "ftp",
+ "javascript",
+ "websocket",
+ "secure websocket",
+ ],
+)
+def test_cookie_unsupported_scheme(session, page):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "domain": page,
+ "path": "/",
+ "httpOnly": False,
+ "secure": False
+ }
+
+ result = add_cookie(session, new_cookie)
+ assert_error(result, "invalid cookie domain")
+
+
+def test_add_domain_cookie(session, url, server_config):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "domain": server_config["browser_host"],
+ "path": "/",
+ "httpOnly": False,
+ "secure": False
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ result = add_cookie(session, new_cookie)
+ assert_success(result)
+
+ cookie = session.cookies("hello")
+ assert "domain" in cookie
+ assert isinstance(cookie["domain"], str)
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+
+ assert cookie["name"] == "hello"
+ assert cookie["value"] == "world"
+ assert cookie["domain"] == server_config["browser_host"] or \
+ cookie["domain"] == ".%s" % server_config["browser_host"]
+
+
+def test_add_cookie_for_ip(session, server_config):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "domain": "127.0.0.1",
+ "path": "/",
+ "httpOnly": False,
+ "secure": False
+ }
+
+ port = server_config["ports"]["http"][0]
+ session.url = f"http://127.0.0.1:{port}/common/blank.html"
+
+ clear_all_cookies(session)
+
+ result = add_cookie(session, new_cookie)
+ assert_success(result)
+
+ cookie = session.cookies("hello")
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "domain" in cookie
+ assert isinstance(cookie["domain"], str)
+
+ assert cookie["name"] == "hello"
+ assert cookie["value"] == "world"
+ assert cookie["domain"] == "127.0.0.1"
+
+
+def test_add_non_session_cookie(session, url):
+ a_day_from_now = int(
+ (datetime.utcnow() + timedelta(days=1) - datetime.utcfromtimestamp(0)).total_seconds())
+
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "expiry": a_day_from_now
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ result = add_cookie(session, new_cookie)
+ assert_success(result)
+
+ cookie = session.cookies("hello")
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "expiry" in cookie
+ assert isinstance(cookie["expiry"], int)
+
+ assert cookie["name"] == "hello"
+ assert cookie["value"] == "world"
+ assert cookie["expiry"] == a_day_from_now
+
+
+def test_add_session_cookie(session, url):
+ new_cookie = {
+ "name": "hello",
+ "value": "world"
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ result = add_cookie(session, new_cookie)
+ assert_success(result)
+
+ cookie = session.cookies("hello")
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ if "expiry" in cookie:
+ assert cookie.get("expiry") is None
+
+ assert cookie["name"] == "hello"
+ assert cookie["value"] == "world"
+
+
+def test_add_session_cookie_with_leading_dot_character_in_domain(session, url, server_config):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "domain": ".%s" % server_config["browser_host"]
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ result = add_cookie(session, new_cookie)
+ assert_success(result)
+
+ cookie = session.cookies("hello")
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "domain" in cookie
+ assert isinstance(cookie["domain"], str)
+
+ assert cookie["name"] == "hello"
+ assert cookie["value"] == "world"
+ assert cookie["domain"] == server_config["browser_host"] or \
+ cookie["domain"] == ".%s" % server_config["browser_host"]
+
+
+@pytest.mark.parametrize("same_site", ["None", "Lax", "Strict"])
+def test_add_cookie_with_valid_samesite_flag(session, url, same_site):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "sameSite": same_site
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ result = add_cookie(session, new_cookie)
+ assert_success(result)
+
+ cookie = session.cookies("hello")
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "sameSite" in cookie
+ assert isinstance(cookie["sameSite"], str)
+
+ assert cookie["name"] == "hello"
+ assert cookie["value"] == "world"
+ assert cookie["sameSite"] == same_site
+
+
+def test_add_cookie_with_invalid_samesite_flag(session, url):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "sameSite": "invalid"
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ response = add_cookie(session, new_cookie)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("same_site", [False, 12, dict()])
+def test_add_cookie_with_invalid_samesite_type(session, url, same_site):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "sameSite": same_site
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ response = add_cookie(session, new_cookie)
+ assert_error(response, "invalid argument")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/add_cookie/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/user_prompts.py
new file mode 100644
index 0000000000..f58aacd02a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/add_cookie/user_prompts.py
@@ -0,0 +1,137 @@
+# META: timeout=long
+
+import pytest
+
+from webdriver.error import NoSuchCookieException
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def add_cookie(session, cookie):
+ return session.transport.send(
+ "POST", "session/{session_id}/cookie".format(**vars(session)),
+ {"cookie": cookie})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, url, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ new_cookie = {
+ "name": "foo",
+ "value": "bar",
+ }
+
+ session.url = url("/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = add_cookie(session, new_cookie)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.cookies("foo")
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, url, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ new_cookie = {
+ "name": "foo",
+ "value": "bar",
+ }
+
+ session.url = url("/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = add_cookie(session, new_cookie)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ with pytest.raises(NoSuchCookieException):
+ assert session.cookies("foo")
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, url, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ new_cookie = {
+ "name": "foo",
+ "value": "bar",
+ }
+
+ session.url = url("/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = add_cookie(session, new_cookie)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ with pytest.raises(NoSuchCookieException):
+ assert session.cookies("foo")
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/back/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/back/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/back/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/back/back.py b/testing/web-platform/tests/webdriver/tests/classic/back/back.py
new file mode 100644
index 0000000000..bfc5e0f4a3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/back/back.py
@@ -0,0 +1,134 @@
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def back(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/back".format(**vars(session)))
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<div>")
+ session.url = inline("<p>")
+
+ response = back(session)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = back(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = back(session)
+ assert_success(response)
+
+
+def test_no_browsing_history(session):
+ response = back(session)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("protocol,parameters", [
+ ("http", ""),
+ ("https", ""),
+ ("https", {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"})
+], ids=["http", "https", "https coop"])
+def test_seen_nodes(session, get_test_page, protocol, parameters):
+ first_page = get_test_page(parameters=parameters, protocol=protocol)
+ second_page = get_test_page(parameters=parameters, protocol=protocol, domain="alt")
+
+ session.url = first_page
+ session.url = second_page
+
+ element = session.find.css("#custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ response = back(session)
+ assert_success(response)
+
+ assert session.url == first_page
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.name
+ with pytest.raises(error.DetachedShadowRootException):
+ shadow_root.find_element("css selector", "in-shadow-dom")
+
+ session.find.css("#custom-element", all=False)
+
+
+def test_data_urls(session, inline):
+ test_pages = [
+ inline("<p id=1>"),
+ inline("<p id=2>"),
+ ]
+
+ for page in test_pages:
+ session.url = page
+ assert session.url == test_pages[1]
+
+ response = back(session)
+ assert_success(response)
+ assert session.url == test_pages[0]
+
+
+def test_fragments(session, url):
+ test_pages = [
+ url("/common/blank.html"),
+ url("/common/blank.html#1234"),
+ url("/common/blank.html#5678"),
+ ]
+
+ for page in test_pages:
+ session.url = page
+ assert session.url == test_pages[2]
+
+ response = back(session)
+ assert_success(response)
+ assert session.url == test_pages[1]
+
+ response = back(session)
+ assert_success(response)
+ assert session.url == test_pages[0]
+
+
+def test_history_pushstate(session, inline):
+ pushstate_page = inline("""
+ <script>
+ function pushState() {
+ history.pushState({foo: "bar"}, "", "#pushstate");
+ }
+ </script>
+ <a onclick="javascript:pushState();">click</a>
+ """)
+
+ session.url = pushstate_page
+ session.find.css("a", all=False).click()
+
+ assert session.url == "{}#pushstate".format(pushstate_page)
+ assert session.execute_script("return history.state;") == {"foo": "bar"}
+
+ response = back(session)
+ assert_success(response)
+
+ assert session.url == pushstate_page
+ assert session.execute_script("return history.state;") is None
+
+
+def test_removed_iframe(session, url, inline):
+ page = inline("<p>foo")
+
+ session.url = page
+ session.url = url("/webdriver/tests/support/html/frames_no_bfcache.html")
+
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ response = back(session)
+ assert_success(response)
+
+ assert session.url == page
diff --git a/testing/web-platform/tests/webdriver/tests/classic/back/conftest.py b/testing/web-platform/tests/webdriver/tests/classic/back/conftest.py
new file mode 100644
index 0000000000..bd5db0cfeb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/back/conftest.py
@@ -0,0 +1,19 @@
+import pytest
+
+from webdriver.error import NoSuchWindowException
+
+
+@pytest.fixture(name="session")
+def fixture_session(capabilities, session):
+ """Prevent re-using existent history by running the test in a new window."""
+ original_handle = session.window_handle
+ session.window_handle = session.new_window()
+
+ yield session
+
+ try:
+ session.window.close()
+ except NoSuchWindowException:
+ pass
+
+ session.window_handle = original_handle
diff --git a/testing/web-platform/tests/webdriver/tests/classic/back/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/back/user_prompts.py
new file mode 100644
index 0000000000..1020a6aaa6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/back/user_prompts.py
@@ -0,0 +1,191 @@
+# META: timeout=long
+
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def back(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/back".format(**vars(session)))
+
+
+@pytest.fixture
+def pages(session, inline):
+ pages = [
+ inline("<p id=1>"),
+ inline("<p id=2>"),
+ ]
+
+ for page in pages:
+ session.url = page
+
+ return pages
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+ page_target = url("/webdriver/tests/support/html/default.html")
+
+ session.url = page_target
+ session.url = page_beforeunload
+
+ element = session.find.css("input", all=False)
+ element.send_keys("bar")
+
+ response = back(session)
+ assert_success(response)
+
+ assert session.url == page_target
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, pages):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = back(session)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.url == pages[0]
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, pages):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = back(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.url == pages[1]
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, pages):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = back(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.url == pages[1]
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/close_window/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/close_window/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/close_window/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/close_window/close.py b/testing/web-platform/tests/webdriver/tests/classic/close_window/close.py
new file mode 100644
index 0000000000..680f471839
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/close_window/close.py
@@ -0,0 +1,102 @@
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def close(session):
+ return session.transport.send(
+ "DELETE", "session/{session_id}/window".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = close(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, url):
+ new_handle = session.new_window()
+
+ session.url = url("/webdriver/tests/support/html/frames.html")
+
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ frame = session.find.css("#delete-frame", all=False)
+ session.switch_frame(frame)
+
+ button = session.find.css("#remove-parent", all=False)
+ button.click()
+
+ response = close(session)
+ handles = assert_success(response)
+ assert handles == [new_handle]
+
+
+def test_close_browsing_context(session):
+ original_handles = session.handles
+
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ response = close(session)
+ handles = assert_success(response, original_handles)
+ assert session.handles == original_handles
+ assert new_handle not in handles
+
+
+def test_close_browsing_context_with_dismissed_beforeunload_prompt(session, inline):
+ original_handles = session.handles
+
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ session.url = inline("""
+ <input type="text">
+ <script>
+ window.addEventListener("beforeunload", function (event) {
+ event.preventDefault();
+ });
+ </script>
+ """)
+
+ session.find.css("input", all=False).send_keys("foo")
+
+ response = close(session)
+ handles = assert_success(response, original_handles)
+ assert session.handles == original_handles
+ assert new_handle not in handles
+
+ # A beforeunload prompt has to be automatically dismissed
+ with pytest.raises(error.NoSuchWindowException):
+ session.alert.text
+
+
+def test_close_last_browsing_context(session):
+ assert len(session.handles) == 1
+ response = close(session)
+
+ assert_success(response, [])
+
+ # With no more open top-level browsing contexts, the session is closed.
+ session.session_id = None
+
+
+def test_element_usage_after_closing_browsing_context(session, inline):
+ session.url = inline("<p id='a'>foo")
+ session.find.css("p", all=False)
+ first = session.window_handle
+
+ second = session.new_window(type_hint="tab")
+ session.window_handle = second
+
+ session.url = inline("<p id='b'>other")
+ b = session.find.css("p", all=False)
+
+ session.window_handle = first
+ response = close(session)
+ assert_success(response)
+ assert len(session.handles) == 1
+
+ session.window_handle = second
+ assert b.attribute("id") == "b"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/close_window/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/close_window/user_prompts.py
new file mode 100644
index 0000000000..37928f9758
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/close_window/user_prompts.py
@@ -0,0 +1,187 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def close(session):
+ return session.transport.send(
+ "DELETE", "session/{session_id}/window".format(**vars(session)))
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ session.url = page_beforeunload
+ element = session.find.css("input", all=False)
+ element.send_keys("bar")
+
+ response = close(session)
+ assert_success(response)
+
+ assert new_handle not in session.handles
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ original_handle = session.window_handle
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = close(session)
+ assert_success(response)
+
+ # Asserting that the dialog was handled requires valid top-level browsing
+ # context, so we must switch to the original window.
+ session.window_handle = original_handle
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert new_handle not in session.handles
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = close(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert new_handle in session.handles
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = close(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert new_handle in session.handles
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/delete.py b/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/delete.py
new file mode 100644
index 0000000000..86d66561b0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/delete.py
@@ -0,0 +1,22 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def delete_all_cookies(session):
+ return session.transport.send(
+ "DELETE", "/session/{session_id}/cookie".format(**vars(session)))
+
+
+def test_null_response_value(session, url):
+ response = delete_all_cookies(session)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = delete_all_cookies(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = delete_all_cookies(session)
+ assert_error(response, "no such window")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/user_prompts.py
new file mode 100644
index 0000000000..03f34d9f6f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_all_cookies/user_prompts.py
@@ -0,0 +1,117 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def delete_all_cookies(session):
+ return session.transport.send(
+ "DELETE", "/session/{session_id}/cookie".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = delete_all_cookies(session)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.cookies() == []
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = delete_all_cookies(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.cookies() != []
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = delete_all_cookies(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.cookies() != []
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/delete.py b/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/delete.py
new file mode 100644
index 0000000000..4b37c0453b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/delete.py
@@ -0,0 +1,29 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def delete_cookie(session, name):
+ return session.transport.send(
+ "DELETE", "/session/{session_id}/cookie/{name}".format(
+ session_id=session.session_id,
+ name=name))
+
+
+def test_null_response_value(session, url):
+ response = delete_cookie(session, "foo")
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = delete_cookie(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = delete_cookie(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_unknown_cookie(session):
+ response = delete_cookie(session, "stilton")
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/user_prompts.py
new file mode 100644
index 0000000000..1ed7db6e8e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_cookie/user_prompts.py
@@ -0,0 +1,119 @@
+# META: timeout=long
+
+import pytest
+
+from webdriver.error import NoSuchCookieException
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def delete_cookie(session, name):
+ return session.transport.send("DELETE", "/session/%s/cookie/%s" % (session.session_id, name))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = delete_cookie(session, "foo")
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ with pytest.raises(NoSuchCookieException):
+ assert session.cookies("foo")
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = delete_cookie(session, "foo")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.cookies("foo")
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = delete_cookie(session, "foo")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.cookies("foo")
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_session/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/delete_session/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_session/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/delete_session/delete.py b/testing/web-platform/tests/webdriver/tests/classic/delete_session/delete.py
new file mode 100644
index 0000000000..a3032cc134
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/delete_session/delete.py
@@ -0,0 +1,42 @@
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_success
+
+
+def delete_session(session):
+ return session.transport.send("DELETE", "session/{session_id}".format(**vars(session)))
+
+
+def test_null_response_value(session):
+ response = delete_session(session)
+ value = assert_success(response)
+ assert value is None
+
+ # Need an explicit call to session.end() to notify the test harness
+ # that a new session needs to be created for subsequent tests.
+ session.end()
+
+
+def test_dismissed_beforeunload_prompt(session, inline):
+ session.url = inline("""
+ <input type="text">
+ <script>
+ window.addEventListener("beforeunload", function (event) {
+ event.preventDefault();
+ });
+ </script>
+ """)
+
+ session.find.css("input", all=False).send_keys("foo")
+
+ response = delete_session(session)
+ assert_success(response)
+
+ # A beforeunload prompt has to be automatically dismissed, and the session deleted
+ with pytest.raises(error.InvalidSessionIdException):
+ session.alert.text
+
+ # Need an explicit call to session.end() to notify the test harness
+ # that a new session needs to be created for subsequent tests.
+ session.end()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/dismiss.py b/testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/dismiss.py
new file mode 100644
index 0000000000..a28dec7687
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/dismiss_alert/dismiss.py
@@ -0,0 +1,109 @@
+import pytest
+
+from webdriver.error import NoSuchAlertException
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import wait_for_new_handle
+from tests.support.sync import Poll
+
+
+def dismiss_alert(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/alert/dismiss".format(**vars(session)))
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<script>window.alert('Hello');</script>")
+
+ response = dismiss_alert(session)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = dismiss_alert(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = dismiss_alert(session)
+ assert_error(response, "no such alert")
+
+
+def test_no_user_prompt(session):
+ response = dismiss_alert(session)
+ assert_error(response, "no such alert")
+
+
+def test_dismiss_alert(session, inline):
+ session.url = inline("<script>window.alert('Hello');</script>")
+
+ response = dismiss_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+
+def test_dismiss_confirm(session, inline):
+ session.url = inline("<script>window.result = window.confirm('Hello');</script>")
+
+ response = dismiss_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+ assert session.execute_script("return window.result;") is False
+
+
+def test_dismiss_prompt(session, inline):
+ session.url = inline("""
+ <script>window.result = window.prompt('Enter Your Name: ', 'Federer');</script>
+ """)
+
+ response = dismiss_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+ assert session.execute_script("return window.result") is None
+
+
+def test_unexpected_alert(session):
+ session.execute_script("setTimeout(function() { alert('Hello'); }, 100);")
+
+ wait = Poll(
+ session,
+ timeout=5,
+ ignored_exceptions=NoSuchAlertException,
+ message="No user prompt with text 'Hello' detected")
+ wait.until(lambda s: s.alert.text == "Hello")
+
+ response = dismiss_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+
+def test_dismiss_in_popup_window(session, inline):
+ orig_handles = session.handles
+
+ session.url = inline("""
+ <button onclick="window.open('about:blank', '_blank', 'width=500; height=200;resizable=yes');">open</button>
+ """)
+ button = session.find.css("button", all=False)
+ button.click()
+
+ session.window_handle = wait_for_new_handle(session, orig_handles)
+ session.url = inline("""
+ <script>window.alert("Hello")</script>
+ """)
+
+ response = dismiss_alert(session)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_clear/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py b/testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py
new file mode 100644
index 0000000000..9a0549ce4f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_clear/clear.py
@@ -0,0 +1,452 @@
+import pytest
+from webdriver import WebElement
+
+from tests.support.asserts import (
+ assert_element_has_focus,
+ assert_error,
+ assert_events_equal,
+ assert_in_events,
+ assert_success,
+)
+
+
+@pytest.fixture
+def tracked_events():
+ return [
+ "blur",
+ "change",
+ "focus",
+ ]
+
+
+def element_clear(session, element):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/clear".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+@pytest.fixture(scope="session")
+def text_file(tmpdir_factory):
+ fh = tmpdir_factory.mktemp("tmp").join("hello.txt")
+ fh.write("hello")
+ return fh
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ element = WebElement(session, "foo")
+ response = element_clear(session, element)
+ assert_error(response, "no such window")
+
+ original_handle, element = closed_window
+ response = element_clear(session, element)
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = element_clear(session, element)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ element = WebElement(session, "foo")
+
+ response = element_clear(session, element)
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = element_clear(session, element)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = element_clear(session, element.shadow_root)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = element_clear(session, element)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = element_clear(session, element)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ response = element_clear(session, element)
+ assert_error(response, "stale element reference")
+
+
+def test_pointer_interactable(session, inline):
+ session.url = inline("<input style='margin-left: -1000px' value=foobar>")
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "element not interactable")
+
+
+def test_keyboard_interactable(session, inline):
+ session.url = inline("""
+ <input value=foobar>
+ <div></div>
+
+ <style>
+ div {
+ position: absolute;
+ background: blue;
+ top: 0;
+ }
+ </style>
+ """)
+ element = session.find.css("input", all=False)
+ assert element.property("value") == "foobar"
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("value") == ""
+
+
+@pytest.mark.parametrize("type,value,default",
+ [("number", "42", ""),
+ ("range", "42", "50"),
+ ("email", "foo@example.com", ""),
+ ("password", "password", ""),
+ ("search", "search", ""),
+ ("tel", "999", ""),
+ ("text", "text", ""),
+ ("url", "https://example.com/", ""),
+ ("color", "#ff0000", "#000000"),
+ ("date", "2017-12-26", ""),
+ ("datetime", "2017-12-26T19:48", ""),
+ ("datetime-local", "2017-12-26T19:48", ""),
+ ("time", "19:48", ""),
+ ("month", "2017-11", ""),
+ ("week", "2017-W52", "")])
+def test_input(session, inline, add_event_listeners, tracked_events, type, value, default):
+ session.url = inline("<input type=%s value='%s'>" % (type, value))
+ element = session.find.css("input", all=False)
+ add_event_listeners(element, tracked_events)
+ assert element.property("value") == value
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("value") == default
+ assert_in_events(session, ["focus", "change", "blur"])
+ assert_element_has_focus(session.execute_script("return document.body"))
+
+
+@pytest.mark.parametrize("type",
+ ["number",
+ "range",
+ "email",
+ "password",
+ "search",
+ "tel",
+ "text",
+ "url",
+ "color",
+ "date",
+ "datetime",
+ "datetime-local",
+ "time",
+ "month",
+ "week",
+ "file"])
+def test_input_disabled(session, inline, type):
+ session.url = inline("<input type=%s disabled>" % type)
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+@pytest.mark.parametrize("type",
+ ["number",
+ "range",
+ "email",
+ "password",
+ "search",
+ "tel",
+ "text",
+ "url",
+ "color",
+ "date",
+ "datetime",
+ "datetime-local",
+ "time",
+ "month",
+ "week",
+ "file"])
+def test_input_readonly(session, inline, type):
+ session.url = inline("<input type=%s readonly>" % type)
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_textarea(session, inline, add_event_listeners, tracked_events):
+ session.url = inline("<textarea>foobar</textarea>")
+ element = session.find.css("textarea", all=False)
+ add_event_listeners(element, tracked_events)
+ assert element.property("value") == "foobar"
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("value") == ""
+ assert_in_events(session, ["focus", "change", "blur"])
+
+
+def test_textarea_disabled(session, inline):
+ session.url = inline("<textarea disabled></textarea>")
+ element = session.find.css("textarea", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_textarea_readonly(session, inline):
+ session.url = inline("<textarea readonly></textarea>")
+ element = session.find.css("textarea", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_input_file(session, text_file, inline):
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+ element.send_keys(str(text_file))
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("value") == ""
+
+
+def test_input_file_multiple(session, text_file, inline):
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+ element.send_keys(str(text_file))
+ element.send_keys(str(text_file))
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("value") == ""
+
+
+def test_select(session, inline):
+ session.url = inline("""
+ <select>
+ <option>foo
+ </select>
+ """)
+ select = session.find.css("select", all=False)
+ option = session.find.css("option", all=False)
+
+ response = element_clear(session, select)
+ assert_error(response, "invalid element state")
+ response = element_clear(session, option)
+ assert_error(response, "invalid element state")
+
+
+def test_button(session, inline):
+ session.url = inline("<button></button>")
+ button = session.find.css("button", all=False)
+
+ response = element_clear(session, button)
+ assert_error(response, "invalid element state")
+
+
+def test_button_with_subtree(session, inline):
+ """
+ Elements inside button elements are interactable.
+ """
+ session.url = inline("""
+ <button>
+ <input value=foobar>
+ </button>
+ """)
+ text_field = session.find.css("input", all=False)
+
+ response = element_clear(session, text_field)
+ assert_success(response)
+
+
+def test_contenteditable(session, inline, add_event_listeners, tracked_events):
+ session.url = inline("<p contenteditable>foobar</p>")
+ element = session.find.css("p", all=False)
+ add_event_listeners(element, tracked_events)
+ assert element.property("innerHTML") == "foobar"
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("innerHTML") == ""
+ assert_events_equal(session, ["focus", "blur"])
+ assert_element_has_focus(session.execute_script("return document.body"))
+
+
+def test_designmode(session, inline):
+ session.url = inline("foobar")
+ element = session.find.css("body", all=False)
+ assert element.property("innerHTML") == "foobar"
+ session.execute_script("document.designMode = 'on'")
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("innerHTML") in ["", "<br>"]
+ assert_element_has_focus(session.execute_script("return document.body"))
+
+
+def test_resettable_element_focus_when_empty(session, inline, add_event_listeners, tracked_events):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+ add_event_listeners(element, tracked_events)
+ assert element.property("value") == ""
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("value") == ""
+ assert_events_equal(session, [])
+
+
+@pytest.mark.parametrize("type,invalid_value",
+ [("number", "foo"),
+ ("range", "foo"),
+ ("email", "foo"),
+ ("url", "foo"),
+ ("color", "foo"),
+ ("date", "foo"),
+ ("datetime", "foo"),
+ ("datetime-local", "foo"),
+ ("time", "foo"),
+ ("month", "foo"),
+ ("week", "foo")])
+def test_resettable_element_does_not_satisfy_validation_constraints(session, inline, type, invalid_value):
+ """
+ Some UAs allow invalid input to certain types of constrained
+ form controls. For example, Gecko allows non-valid characters
+ to be typed into <input type=number> but Chrome does not.
+ Since we want to test that Element Clear works for clearing the
+ invalid characters in these UAs, it is fine to skip this test
+ where UAs do not allow the element to not satisfy its constraints.
+ """
+ session.url = inline("<input type=%s>" % type)
+ element = session.find.css("input", all=False)
+
+ def is_valid(element):
+ return session.execute_script("""
+ var input = arguments[0];
+ return input.validity.valid;
+ """, args=(element,))
+
+ # value property does not get updated if the input is invalid
+ element.send_keys(invalid_value)
+
+ # UA does not allow invalid input for this form control type
+ if is_valid(element):
+ return
+
+ response = element_clear(session, element)
+ assert_success(response)
+ assert is_valid(element)
+
+
+@pytest.mark.parametrize("type",
+ ["checkbox",
+ "radio",
+ "hidden",
+ "submit",
+ "button",
+ "image"])
+def test_non_editable_inputs(session, inline, type):
+ session.url = inline("<input type=%s>" % type)
+ element = session.find.css("input", all=False)
+
+ response = element_clear(session, element)
+ assert_error(response, "invalid element state")
+
+
+def test_scroll_into_view(session, inline):
+ session.url = inline("""
+ <input value=foobar>
+ <div style='height: 200vh; width: 5000vh'>
+ """)
+ element = session.find.css("input", all=False)
+ assert element.property("value") == "foobar"
+ assert session.execute_script("return window.pageYOffset") == 0
+
+ # scroll to the bottom right of the page
+ session.execute_script("""
+ var body = document.body;
+ window.scrollTo(body.scrollWidth, body.scrollHeight);
+ """)
+
+ # clear and scroll back to the top of the page
+ response = element_clear(session, element)
+ assert_success(response)
+ assert element.property("value") == ""
+
+ # check if element cleared is scrolled into view
+ rect = session.execute_script("""
+ var input = arguments[0];
+ var rect = input.getBoundingClientRect();
+ return {"top": rect.top,
+ "left": rect.left,
+ "height": rect.height,
+ "width": rect.width};
+ """, args=(element,))
+ window = session.execute_script("""
+ return {"innerHeight": window.innerHeight,
+ "innerWidth": window.innerWidth,
+ "pageXOffset": window.pageXOffset,
+ "pageYOffset": window.pageYOffset};
+ """)
+
+ assert rect["top"] < (window["innerHeight"] + window["pageYOffset"]) and \
+ rect["left"] < (window["innerWidth"] + window["pageXOffset"]) and \
+ (rect["top"] + element.rect["height"]) > window["pageYOffset"] and \
+ (rect["left"] + element.rect["width"]) > window["pageXOffset"]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py
new file mode 100644
index 0000000000..7a8564a684
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_clear/user_prompts.py
@@ -0,0 +1,131 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def element_clear(session, element):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/clear".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+ element.send_keys("foo")
+
+ assert element.property("value") == "foo"
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_clear(session, element)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert element.property("value") == ""
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+ element.send_keys("foo")
+
+ assert element.property("value") == "foo"
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_clear(session, element)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert element.property("value") == "foo"
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+ element.send_keys("foo")
+
+ assert element.property("value") == "foo"
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_clear(session, element)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert element.property("value") == "foo"
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/bubbling.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/bubbling.py
new file mode 100644
index 0000000000..7620ec3224
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/bubbling.py
@@ -0,0 +1,157 @@
+from tests.support.asserts import assert_success
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+def test_click_event_bubbles_to_parents(session, inline):
+ session.url = inline("""
+ <style>
+ body * {
+ margin: 10px;
+ padding: 10px;
+ border: 1px solid blue;
+ }
+ </style>
+
+ <div id=three>THREE
+ <div id=two>TWO
+ <div id=one>ONE</div>
+ </div>
+ </div>
+
+ <script>
+ window.clicks = [];
+
+ var elements = document.querySelectorAll("div");
+ for (var level = 0; level < elements.length; level++) {
+ elements[level].addEventListener("click", function(clickEvent) {
+ window.clicks.push(clickEvent.currentTarget);
+ });
+ }
+ </script>
+ """)
+ three, two, one = session.find.css("div")
+ one.click()
+
+ clicks = session.execute_script("return window.clicks")
+ assert one in clicks
+ assert two in clicks
+ assert three in clicks
+
+
+def test_spin_event_loop(session, inline):
+ """
+ Wait until the user agent event loop has spun enough times to
+ process the DOM events generated by clicking.
+ """
+ session.url = inline("""
+ <style>
+ body * {
+ margin: 10px;
+ padding: 10px;
+ border: 1px solid blue;
+ }
+ </style>
+
+ <div id=three>THREE
+ <div id=two>TWO
+ <div id=one>ONE</div>
+ </div>
+ </div>
+
+ <script>
+ window.delayedClicks = [];
+
+ var elements = document.querySelectorAll("div");
+ for (var level = 0; level < elements.length; level++) {
+ elements[level].addEventListener("click", function(clickEvent) {
+ var target = clickEvent.currentTarget;
+ setTimeout(function() { window.delayedClicks.push(target); }, 0);
+ });
+ }
+ </script>
+ """)
+ three, two, one = session.find.css("div")
+ one.click()
+
+ delayed_clicks = session.execute_script("return window.delayedClicks")
+ assert one in delayed_clicks
+ assert two in delayed_clicks
+ assert three in delayed_clicks
+
+
+def test_element_disappears_during_click(session, inline):
+ """
+ When an element in the event bubbling order disappears (its CSS
+ display style is set to "none") during a click, Gecko and Blink
+ exhibit different behaviour. Whilst Chrome fires a "click"
+ DOM event on <body>, Firefox does not.
+
+ A WebDriver implementation may choose to wait for this event to let
+ the event loops spin enough times to let click events propagate,
+ so this is a corner case test that Firefox does not hang indefinitely.
+ """
+ session.url = inline("""
+ <style>
+ #over,
+ #under {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ width: 100px;
+ height: 100px;
+ }
+
+ #over {
+ background: blue;
+ opacity: .5;
+ }
+ #under {
+ background: yellow;
+ }
+
+ #log {
+ margin-top: 120px;
+ }
+ </style>
+
+ <body id="body">
+ <div id=under></div>
+ <div id=over></div>
+
+ <div id=log></div>
+ </body>
+
+ <script>
+ let under = document.querySelector("#under");
+ let over = document.querySelector("#over");
+ let body = document.querySelector("body");
+ let log = document.querySelector("#log");
+
+ function logEvent({type, target, currentTarget}) {
+ log.innerHTML += "<p></p>";
+ log.lastElementChild.textContent =
+ `${type} in ${target.id} (handled by ${currentTarget.id})`;
+ }
+
+ for (let ev of ["click", "mousedown", "mouseup"]) {
+ under.addEventListener(ev, logEvent);
+ over.addEventListener(ev, logEvent);
+ body.addEventListener(ev, logEvent);
+ }
+
+ over.addEventListener("mousedown", function(mousedownEvent) {
+ over.style.display = "none";
+ });
+ </script>
+ """)
+ over = session.find.css("#over", all=False)
+
+ # should not time out
+ response = element_click(session, over)
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/center_point.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/center_point.py
new file mode 100644
index 0000000000..86b54a2d42
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/center_point.py
@@ -0,0 +1,64 @@
+import pytest
+
+from tests.support.asserts import assert_success
+from tests.support.helpers import center_point
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+def square(inline, size):
+ return inline("""
+ <style>
+ body {{ margin: 0 }}
+
+ div {{
+ background: blue;
+ width: {size}px;
+ height: {size}px;
+ }}
+ </style>
+
+ <div id=target></div>
+
+ <script>
+ window.clicks = [];
+ let div = document.querySelector("div");
+ div.addEventListener("click", function(e) {{ window.clicks.push([e.clientX, e.clientY]) }});
+ </script>
+ """.format(size=size))
+
+
+def assert_one_click(session):
+ """Asserts there has only been one click, and returns that."""
+ clicks = session.execute_script("return window.clicks")
+ assert len(clicks) == 1
+ return tuple(clicks[0])
+
+
+def test_entirely_in_view(session, inline):
+ session.url = square(inline, 300)
+ element = session.find.css("#target", all=False)
+
+ response = element_click(session, element)
+ assert_success(response)
+
+ click_point = assert_one_click(session)
+ assert click_point == (150, 150)
+
+
+@pytest.mark.parametrize("size", range(1, 11))
+def test_css_pixel_rounding(session, inline, size):
+ session.url = square(inline, size)
+ element = session.find.css("#target", all=False)
+ expected_click_point = center_point(element)
+
+ response = element_click(session, element)
+ assert_success(response)
+
+ actual_click_point = assert_one_click(session)
+ assert actual_click_point == expected_click_point
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/click.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/click.py
new file mode 100644
index 0000000000..61acc923e8
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/click.py
@@ -0,0 +1,99 @@
+import pytest
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<p>foo")
+ element = session.find.css("p", all=False)
+
+ response = element_click(session, element)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ element = WebElement(session, "foo")
+ response = element_click(session, element)
+ assert_error(response, "no such window")
+
+ original_handle, element = closed_window
+ response = element_click(session, element)
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = element_click(session, element)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ element = WebElement(session, "foo")
+
+ response = element_click(session, element)
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = element_click(session, element)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = element_click(session, element.shadow_root)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = element_click(session, element)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("input#text", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = element_click(session, element)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ response = element_click(session, element)
+ assert_error(response, "stale element reference")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/events.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/events.py
new file mode 100644
index 0000000000..e6900f2d29
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/events.py
@@ -0,0 +1,34 @@
+from tests.support.asserts import assert_success
+from tests.support.helpers import filter_dict
+
+def get_events(session):
+ """Return list of mouse events recorded in the fixture."""
+ return session.execute_script("return allEvents.events;") or []
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+def test_event_mousemove(session, url):
+ session.url = url(
+ "/webdriver/tests/classic/element_click/support/test_click_wdspec.html"
+ )
+
+ element = session.find.css('#outer', all=False)
+ response = element_click(session, element)
+ assert_success(response)
+
+ events = get_events(session)
+ assert len(events) == 4
+
+ expected = [
+ {"type": "mousemove", "buttons": 0, "button": 0},
+ {"type": "mousedown", "buttons": 1, "button": 0},
+ {"type": "mouseup", "buttons": 0, "button": 0},
+ {"type": "click", "buttons": 0, "button": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+
+ assert expected == filtered_events
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/file_upload.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/file_upload.py
new file mode 100644
index 0000000000..50e30085e1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/file_upload.py
@@ -0,0 +1,16 @@
+from tests.support.asserts import assert_error
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+def test_file_upload_state(session, inline):
+ session.url = inline("<input type=file>")
+
+ element = session.find.css("input", all=False)
+ response = element_click(session, element)
+ assert_error(response, "invalid argument")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/interactability.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/interactability.py
new file mode 100644
index 0000000000..d55860c874
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/interactability.py
@@ -0,0 +1,130 @@
+import pytest
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+def test_display_none(session, inline):
+ session.url = inline("""<button style="display: none">foobar</button>""")
+ element = session.find.css("button", all=False)
+
+ response = element_click(session, element)
+ assert_error(response, "element not interactable")
+
+
+def test_visibility_hidden(session, inline):
+ session.url = inline("""<button style="visibility: hidden">foobar</button>""")
+ element = session.find.css("button", all=False)
+
+ response = element_click(session, element)
+ assert_error(response, "element not interactable")
+
+
+def test_hidden(session, inline):
+ session.url = inline("<button hidden>foobar</button>")
+ element = session.find.css("button", all=False)
+
+ response = element_click(session, element)
+ assert_error(response, "element not interactable")
+
+
+def test_disabled(session, inline):
+ session.url = inline("""<button disabled>foobar</button>""")
+ element = session.find.css("button", all=False)
+
+ response = element_click(session, element)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("transform", ["translate(-100px, -100px)", "rotate(50deg)"])
+def test_element_not_interactable_css_transform(session, inline, transform):
+ session.url = inline("""
+ <div style="width: 500px; height: 100px;
+ background-color: blue; transform: {transform};">
+ <input type=button>
+ </div>""".format(transform=transform))
+ element = session.find.css("input", all=False)
+ response = element_click(session, element)
+ assert_error(response, "element not interactable")
+
+
+def test_element_not_interactable_out_of_view(session, inline):
+ session.url = inline("""
+ <style>
+ input {
+ position: absolute;
+ margin-top: -100vh;
+ background: red;
+ }
+ </style>
+
+ <input>
+ """)
+ element = session.find.css("input", all=False)
+ response = element_click(session, element)
+ assert_error(response, "element not interactable")
+
+
+@pytest.mark.parametrize("tag_name", ["div", "span"])
+def test_zero_sized_element(session, inline, tag_name):
+ session.url = inline("<{0}></{0}>".format(tag_name))
+ element = session.find.css(tag_name, all=False)
+
+ response = element_click(session, element)
+ assert_error(response, "element not interactable")
+
+
+def test_element_intercepted(session, inline):
+ session.url = inline("""
+ <style>
+ div {
+ position: absolute;
+ height: 100vh;
+ width: 100vh;
+ background: blue;
+ top: 0;
+ left: 0;
+ }
+ </style>
+
+ <input type=button value=Roger>
+ <div></div>
+ """)
+ element = session.find.css("input", all=False)
+ response = element_click(session, element)
+ assert_error(response, "element click intercepted")
+
+
+def test_element_intercepted_no_pointer_events(session, inline):
+ session.url = inline("""<input type=button value=Roger style="pointer-events: none">""")
+ element = session.find.css("input", all=False)
+ response = element_click(session, element)
+ assert_error(response, "element click intercepted")
+
+
+def test_element_not_visible_overflow_hidden(session, inline):
+ session.url = inline("""
+ <style>
+ div {
+ overflow: hidden;
+ height: 50px;
+ background: green;
+ }
+
+ input {
+ margin-top: 100px;
+ background: red;
+ }
+ </style>
+
+ <div><input></div>
+ """)
+ element = session.find.css("input", all=False)
+ response = element_click(session, element)
+ assert_error(response, "element not interactable")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py
new file mode 100644
index 0000000000..6fadee9869
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/navigate.py
@@ -0,0 +1,189 @@
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_success
+from tests.support.helpers import wait_for_new_handle
+from tests.support.sync import Poll
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+def test_numbers_link(session, inline, url):
+ link = "/webdriver/tests/classic/element_click/support/input.html"
+ session.url = inline(f"<a href={link}>123456</a>")
+ element = session.find.css("a", all=False)
+ response = element_click(session, element)
+ assert_success(response)
+
+ assert session.url == url(link)
+
+
+def test_multi_line_link(session, inline, url):
+ link = "/webdriver/tests/classic/element_click/support/input.html"
+ session.url = inline(f"""
+ <p style="background-color: yellow; width: 50px;">
+ <a href={link}>Helloooooooooooooooooooo Worlddddddddddddddd</a>
+ </p>""")
+ element = session.find.css("a", all=False)
+ response = element_click(session, element)
+ assert_success(response)
+
+ assert session.url == url(link)
+
+
+def test_link_unload_event(session, url, server_config, inline):
+ link = "/webdriver/tests/classic/element_click/support/input.html"
+ session.url = inline(f"""
+ <body onunload="checkUnload()">
+ <a href="{link}">click here</a>
+ <input type="checkbox">
+ <script>
+ function checkUnload() {{
+ document.getElementsByTagName("input")[0].checked = true;
+ }}
+ </script>
+ </body>""")
+
+ element = session.find.css("a", all=False)
+ response = element_click(session, element)
+ assert_success(response)
+
+ assert session.url == url(link)
+
+ session.back()
+
+ element = session.find.css("input", all=False)
+ response = session.execute_script("""
+ let input = arguments[0];
+ return input.checked;
+ """, args=(element,))
+ assert response is True
+
+
+def test_link_hash(session, inline):
+ id = "anchor"
+ session.url = inline("""
+ <a href="#{url}">aaaa</a>
+ <p id={id} style="margin-top: 5000vh">scroll here</p>
+ """.format(url=id, id=id))
+ old_url = session.url
+
+ element = session.find.css("a", all=False)
+ response = element_click(session, element)
+ assert_success(response)
+
+ new_url = session.url
+ assert "{url}#{id}".format(url=old_url, id=id) == new_url
+
+ element = session.find.css("p", all=False)
+ assert session.execute_script("""
+ let input = arguments[0];
+ rect = input.getBoundingClientRect();
+ return rect["top"] >= 0 && rect["left"] >= 0 &&
+ (rect["top"] + rect["height"]) <= window.innerHeight &&
+ (rect["left"] + rect["width"]) <= window.innerWidth;
+ """, args=(element,)) is True
+
+
+@pytest.mark.parametrize("target", [
+ "",
+ "_blank",
+ "_parent",
+ "_self",
+ "_top",
+])
+def test_link_from_toplevel_context_with_target(session, inline, target):
+ target_page = inline("<p id='foo'>foo</p>")
+
+ session.url = inline("<a href='{}' target='{}'>click</a>".format(target_page, target))
+ element = session.find.css("a", all=False)
+
+ orig_handles = session.handles
+
+ response = element_click(session, element)
+ assert_success(response)
+
+ if target == "_blank":
+ session.window_handle = wait_for_new_handle(session, orig_handles)
+
+ wait = Poll(
+ session,
+ timeout=5,
+ ignored_exceptions=error.NoSuchElementException,
+ message="Expected element has not been found")
+ wait.until(lambda s: s.find.css("#foo"))
+
+
+@pytest.mark.parametrize("target", [
+ "",
+ "_blank",
+ "_parent",
+ "_self",
+ "_top",
+])
+def test_link_from_nested_context_with_target(session, inline, iframe, target):
+ target_page = inline("<p id='foo'>foo</p>")
+
+ session.url = inline(iframe("<a href='{}' target='{}'>click</a>".format(target_page, target)))
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+ element = session.find.css("a", all=False)
+
+ orig_handles = session.handles
+
+ response = element_click(session, element)
+ assert_success(response)
+
+ if target == "_blank":
+ session.window_handle = wait_for_new_handle(session, orig_handles)
+
+ # With the current browsing context removed the navigation should
+ # not timeout. Switch to the target context, and wait until the expected
+ # element is available.
+ if target == "_parent":
+ session.switch_frame("parent")
+ elif target == "_top":
+ session.switch_frame(None)
+
+ wait = Poll(
+ session,
+ timeout=5,
+ ignored_exceptions=error.NoSuchElementException,
+ message="Expected element has not been found")
+ wait.until(lambda s: s.find.css("#foo"))
+
+
+def test_link_cross_origin(session, inline, url):
+ base_path = ("/webdriver/tests/support/html/subframe.html" +
+ "?pipe=header(Cross-Origin-Opener-Policy,same-origin)")
+ target_page = url(base_path, protocol="https", domain="alt")
+
+ session.url = inline("<a href='{}'>click me</a>".format(target_page), protocol="https")
+ link = session.find.css("a", all=False)
+
+ response = element_click(session, link)
+ assert_success(response)
+
+ assert session.url == target_page
+ with pytest.raises(error.StaleElementReferenceException):
+ link.click()
+
+ session.find.css("#delete", all=False)
+
+
+def test_link_closes_window(session, inline):
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ session.url = inline("""<a href="javascript:window.close()">Close me</a>""")
+ element = session.find.css("a", all=False)
+
+ response = element_click(session, element)
+ assert_success(response)
+
+ assert new_handle not in session.handles
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/scroll_into_view.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/scroll_into_view.py
new file mode 100644
index 0000000000..591847e881
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/scroll_into_view.py
@@ -0,0 +1,72 @@
+import pytest
+
+from tests.support.asserts import assert_success
+from tests.support.helpers import center_point
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+def assert_one_click(session):
+ """Asserts there has only been one click, and returns that."""
+ clicks = session.execute_script("return window.clicks")
+ assert len(clicks) == 1
+ return tuple(clicks[0])
+
+
+def test_scroll_into_view(session, inline):
+ session.url = inline("""
+ <input type=text value=Federer
+ style="position: absolute; left: 0vh; top: 500vh">""")
+
+ element = session.find.css("input", all=False)
+ response = element_click(session, element)
+ assert_success(response)
+
+ # Check if element clicked is scrolled into view
+ assert session.execute_script("""
+ let input = arguments[0];
+ rect = input.getBoundingClientRect();
+ return rect["top"] >= 0 && rect["left"] >= 0 &&
+ (rect["top"] + rect["height"]) <= window.innerHeight &&
+ (rect["left"] + rect["width"]) <= window.innerWidth;
+ """, args=(element,)) is True
+
+
+@pytest.mark.parametrize("offset", range(9, 0, -1))
+def test_partially_visible_does_not_scroll(session, offset, inline):
+ session.url = inline("""
+ <style>
+ body {{
+ margin: 0;
+ padding: 0;
+ }}
+
+ div {{
+ background: blue;
+ height: 200px;
+
+ /* make N pixels visible in the viewport */
+ margin-top: calc(100vh - {offset}px);
+ }}
+ </style>
+
+ <div></div>
+
+ <script>
+ window.clicks = [];
+ let target = document.querySelector("div");
+ target.addEventListener("click", function(e) {{ window.clicks.push([e.clientX, e.clientY]); }});
+ </script>
+ """.format(offset=offset))
+ target = session.find.css("div", all=False)
+ assert session.execute_script("return window.scrollY || document.documentElement.scrollTop") == 0
+ response = element_click(session, target)
+ assert_success(response)
+ assert session.execute_script("return window.scrollY || document.documentElement.scrollTop") == 0
+ click_point = assert_one_click(session)
+ assert click_point == center_point(target)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/select.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/select.py
new file mode 100644
index 0000000000..62d40755b5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/select.py
@@ -0,0 +1,223 @@
+def test_click_option(session, inline):
+ session.url = inline("""
+ <select>
+ <option>first
+ <option>second
+ </select>""")
+ options = session.find.css("option")
+
+ assert options[0].selected
+ assert not options[1].selected
+
+ options[1].click()
+ assert options[1].selected
+ assert not options[0].selected
+
+
+def test_click_multiple_option(session, inline):
+ session.url = inline("""
+ <select multiple>
+ <option>first
+ <option>second
+ </select>""")
+ options = session.find.css("option")
+
+ assert not options[0].selected
+ assert not options[1].selected
+
+ options[0].click()
+ assert options[0].selected
+ assert not options[1].selected
+
+
+def test_click_preselected_option(session, inline):
+ session.url = inline("""
+ <select>
+ <option>first
+ <option selected>second
+ </select>""")
+ options = session.find.css("option")
+
+ assert not options[0].selected
+ assert options[1].selected
+
+ options[1].click()
+ assert options[1].selected
+ assert not options[0].selected
+
+ options[0].click()
+ assert options[0].selected
+ assert not options[1].selected
+
+
+def test_click_preselected_multiple_option(session, inline):
+ session.url = inline("""
+ <select multiple>
+ <option>first
+ <option selected>second
+ </select>""")
+ options = session.find.css("option")
+
+ assert not options[0].selected
+ assert options[1].selected
+
+ options[1].click()
+ assert not options[1].selected
+ assert not options[0].selected
+
+ options[0].click()
+ assert options[0].selected
+ assert not options[1].selected
+
+
+def test_click_deselects_others(session, inline):
+ session.url = inline("""
+ <select>
+ <option>first
+ <option>second
+ <option>third
+ </select>""")
+ options = session.find.css("option")
+
+ options[0].click()
+ assert options[0].selected
+ options[1].click()
+ assert options[1].selected
+ options[2].click()
+ assert options[2].selected
+ options[0].click()
+ assert options[0].selected
+
+
+def test_click_multiple_does_not_deselect_others(session, inline):
+ session.url = inline("""
+ <select multiple>
+ <option>first
+ <option>second
+ <option>third
+ </select>""")
+ options = session.find.css("option")
+
+ options[0].click()
+ assert options[0].selected
+ options[1].click()
+ assert options[0].selected
+ assert options[1].selected
+ options[2].click()
+ assert options[0].selected
+ assert options[1].selected
+ assert options[2].selected
+
+
+def test_click_selected_option(session, inline):
+ session.url = inline("""
+ <select>
+ <option>first
+ <option>second
+ </select>""")
+ options = session.find.css("option")
+
+ # First <option> is selected in dropdown
+ assert options[0].selected
+ assert not options[1].selected
+
+ options[1].click()
+ assert options[1].selected
+ options[1].click()
+ assert options[1].selected
+
+
+def test_click_selected_multiple_option(session, inline):
+ session.url = inline("""
+ <select multiple>
+ <option>first
+ <option>second
+ </select>""")
+ options = session.find.css("option")
+
+ # No implicitly selected <option> in <select multiple>
+ assert not options[0].selected
+ assert not options[1].selected
+
+ options[0].click()
+ assert options[0].selected
+ assert not options[1].selected
+
+ # Second click in <select multiple> deselects
+ options[0].click()
+ assert not options[0].selected
+ assert not options[1].selected
+
+
+def test_out_of_view_dropdown(session, inline):
+ session.url = inline("""
+ <select>
+ <option>1
+ <option>2
+ <option>3
+ <option>4
+ <option>5
+ <option>6
+ <option>7
+ <option>8
+ <option>9
+ <option>10
+ <option>11
+ <option>12
+ <option>13
+ <option>14
+ <option>15
+ <option>16
+ <option>17
+ <option>18
+ <option>19
+ <option>20
+ </select>""")
+ options = session.find.css("option")
+
+ options[14].click()
+ assert options[14].selected
+
+
+def test_out_of_view_multiple(session, inline):
+ session.url = inline("""
+ <select multiple>
+ <option>1
+ <option>2
+ <option>3
+ <option>4
+ <option>5
+ <option>6
+ <option>7
+ <option>8
+ <option>9
+ <option>10
+ <option>11
+ <option>12
+ <option>13
+ <option>14
+ <option>15
+ <option>16
+ <option>17
+ <option>18
+ <option>19
+ <option>20
+ </select>""")
+ options = session.find.css("option")
+
+ last_option = options[-1]
+ last_option.click()
+ assert last_option.selected
+
+
+def test_option_disabled(session, inline):
+ session.url = inline("""
+ <select>
+ <option disabled>foo
+ <option>bar
+ </select>""")
+ option = session.find.css("option", all=False)
+ assert not option.selected
+
+ option.click()
+ assert not option.selected
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/shadow_dom.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/shadow_dom.py
new file mode 100644
index 0000000000..6aad49d5c9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/shadow_dom.py
@@ -0,0 +1,60 @@
+import pytest
+from tests.support.asserts import assert_success
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id, element_id=element.id))
+
+
+@pytest.mark.parametrize("click_on", ["host_element", "checkbox_element"])
+def test_shadow_element_click(session, get_test_page, click_on):
+ session.url = get_test_page()
+
+ host_element = session.find.css("custom-element", all=False)
+ checkbox_element = session.execute_script("""
+ return arguments[0].shadowRoot.querySelector("input")
+ """,
+ args=(host_element, ))
+
+ is_pre_checked = session.execute_script("""
+ return arguments[0].checked
+ """,
+ args=(checkbox_element, ))
+ assert is_pre_checked is False
+
+ response = element_click(session, locals()[click_on])
+ assert_success(response)
+
+ is_post_checked = session.execute_script("""
+ return arguments[0].checked
+ """,
+ args=(checkbox_element, ))
+ assert is_post_checked is True
+
+
+@pytest.mark.parametrize("click_on",
+ ["outer_element", "inner_element", "checkbox"])
+def test_nested_shadow_element_click(session, get_test_page, click_on):
+ session.url = get_test_page(nested_shadow_dom=True)
+
+ outer_element = session.find.css("custom-element", all=False)
+ inner_element = session.execute_script("""
+ return arguments[0].shadowRoot.querySelector("inner-custom-element")
+ """,
+ args=(outer_element, ))
+ checkbox = session.execute_script("""
+ return arguments[0].shadowRoot.querySelector("input")
+ """,
+ args=(inner_element, ))
+
+ is_pre_checked = session.execute_script("return arguments[0].checked",
+ args=(checkbox, ))
+ assert is_pre_checked is False
+
+ click_response = element_click(session, locals()[click_on])
+ assert_success(click_response)
+ is_post_checked = session.execute_script("return arguments[0].checked",
+ args=(checkbox, ))
+ assert is_post_checked is True
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/support/input.html b/testing/web-platform/tests/webdriver/tests/classic/element_click/support/input.html
new file mode 100644
index 0000000000..e2c6dadd12
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/support/input.html
@@ -0,0 +1,3 @@
+<html>
+ <input type=text value="Hello World">
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/support/test_click_wdspec.html b/testing/web-platform/tests/webdriver/tests/classic/element_click/support/test_click_wdspec.html
new file mode 100644
index 0000000000..7c6eb6e6e2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/support/test_click_wdspec.html
@@ -0,0 +1,100 @@
+<!doctype html>
+<meta charset=utf-8>
+<html>
+<head>
+ <title>Test Element Click</title>
+ <style>
+ div { padding: 0; margin: 0; }
+ #trackPointer { position: fixed; }
+ #resultContainer { width: 600px; height: 60px; }
+ .area { width: 100px; height: 50px; background-color: #ccc; }
+ .block { width: 5px; height: 5px; border: solid 1px red; }
+ </style>
+ <script>
+ "use strict";
+ var els = {};
+ var allEvents = { events: [] };
+ function displayMessage(message) {
+ document.getElementById("events").innerHTML = "<p>" + message + "</p>";
+ }
+
+ function appendMessage(message) {
+ document.getElementById("events").innerHTML += "<p>" + message + "</p>";
+ }
+
+ function recordPointerEvent(event) {
+ if (event.type === "contextmenu") {
+ event.preventDefault();
+ }
+ allEvents.events.push({
+ "type": event.type,
+ "button": event.button,
+ "buttons": event.buttons,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "ctrlKey": event.ctrlKey,
+ "metaKey": event.metaKey,
+ "altKey": event.altKey,
+ "shiftKey": event.shiftKey,
+ "target": event.target.id
+ });
+ appendMessage(event.type + " " +
+ "button: " + event.button + ", " +
+ "pageX: " + event.pageX + ", " +
+ "pageY: " + event.pageY + ", " +
+ "button: " + event.button + ", " +
+ "buttons: " + event.buttons + ", " +
+ "ctrlKey: " + event.ctrlKey + ", " +
+ "altKey: " + event.altKey + ", " +
+ "metaKey: " + event.metaKey + ", " +
+ "shiftKey: " + event.shiftKey + ", " +
+ "target id: " + event.target.id);
+ }
+
+ function recordFirstPointerMove(event) {
+ recordPointerEvent(event);
+ window.removeEventListener("mousemove", recordFirstPointerMove);
+ }
+
+ function resetEvents() {
+ allEvents.events.length = 0;
+ displayMessage("");
+ }
+
+ function move(el, offsetX, offsetY, timeout) {
+ return function(event) {
+ setTimeout(function() {
+ el.style.top = event.clientY + offsetY + "px";
+ el.style.left = event.clientX + offsetX + "px";
+ }, timeout);
+ };
+ }
+
+ document.addEventListener("DOMContentLoaded", function() {
+ var outer = document.getElementById("outer");
+ window.addEventListener("mousemove", recordFirstPointerMove);
+ outer.addEventListener("click", recordPointerEvent);
+ outer.addEventListener("dblclick", recordPointerEvent);
+ outer.addEventListener("mousedown", recordPointerEvent);
+ outer.addEventListener("mouseup", recordPointerEvent);
+ outer.addEventListener("contextmenu", recordPointerEvent);
+
+ //visual cue for mousemove
+ var pointer = document.getElementById("trackPointer");
+ window.addEventListener("mousemove", move(pointer, 15, 15, 30));
+ });
+ </script>
+</head>
+<body>
+ <div id="trackPointer" class="block"></div>
+ <div>
+ <h2>ClickReporter</h2>
+ <div id="outer" class="area">
+ </div>
+ </div>
+ <div id="resultContainer">
+ <h2>Events</h2>
+ <div id="events"></div>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_click/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/element_click/user_prompts.py
new file mode 100644
index 0000000000..a4c62cbca7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_click/user_prompts.py
@@ -0,0 +1,198 @@
+# META: timeout=long
+
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def element_click(session, element):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/click".format(
+ session_id=session.session_id,
+ element_id=element.id))
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+ page_target = url("/webdriver/tests/support/html/default.html")
+
+ session.url = page_beforeunload
+ input = session.find.css("input", all=False)
+ input.send_keys("bar")
+
+ link = session.find.css("a", all=False)
+ response = element_click(session, link)
+ assert_success(response)
+
+ assert session.url == page_target
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_click(session, element)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.active_element == element
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_click(session, element)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.active_element != element
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_click(session, element)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.active_element != element
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type,
+ retval,
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/__init__.py
new file mode 100644
index 0000000000..a7facf6fcf
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/__init__.py
@@ -0,0 +1,2 @@
+def map_files_to_multiline_text(files):
+ return "\n".join(map(lambda f: str(f), files))
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/conftest.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/conftest.py
new file mode 100644
index 0000000000..17bdd162a7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/conftest.py
@@ -0,0 +1,17 @@
+import pytest
+
+
+@pytest.fixture
+def create_files(tmpdir_factory):
+ def inner(filenames):
+ filelist = []
+ tmpdir = tmpdir_factory.mktemp("tmp")
+ for filename in filenames:
+ fh = tmpdir.join(filename)
+ fh.write(filename)
+ filelist.append(fh)
+
+ return filelist
+
+ inner.__name__ = "create_files"
+ return inner
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/content_editable.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/content_editable.py
new file mode 100644
index 0000000000..9db19d5b8a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/content_editable.py
@@ -0,0 +1,30 @@
+from tests.support.asserts import assert_element_has_focus
+
+
+def test_sets_insertion_point_to_end(session, inline):
+ session.url = inline('<div contenteditable=true>Hello,</div>')
+ body = session.find.css("body", all=False)
+ assert_element_has_focus(body)
+
+ input = session.find.css("div", all=False)
+ input.send_keys(' world!')
+ text = session.execute_script('return arguments[0].textContent', args=[input])
+ assert "Hello, world!" == text.strip()
+ assert_element_has_focus(input)
+
+
+def test_sets_insertion_point_to_after_last_text_node(session, inline):
+ session.url = inline('<div contenteditable=true>Hel<span>lo</span>,</div>')
+ input = session.find.css("div", all=False)
+ input.send_keys(" world!")
+ text = session.execute_script("return arguments[0].textContent", args=[input])
+ assert "Hello, world!" == text.strip()
+
+
+def test_no_move_caret_if_focused(session, inline):
+ session.url = inline("""<div contenteditable=true>Hel<span>lo</span>,</div>
+<script>document.getElementsByTagName("div")[0].focus()</script>""")
+ input = session.find.css("div", all=False)
+ input.send_keys("world!")
+ text = session.execute_script("return arguments[0].textContent", args=[input])
+ assert "world!Hello," == text.strip()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/events.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/events.py
new file mode 100644
index 0000000000..4be1432bf3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/events.py
@@ -0,0 +1,85 @@
+import pytest
+
+from tests.support.asserts import (
+ assert_element_has_focus,
+ assert_events_equal,
+ assert_success,
+)
+
+from . import map_files_to_multiline_text
+
+
+@pytest.fixture
+def tracked_events():
+ return [
+ "blur",
+ "change",
+ "focus",
+ "input",
+ "keydown",
+ "keypress",
+ "keyup",
+ ]
+
+
+def element_send_keys(session, element, text):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id,
+ element_id=element.id),
+ {"text": text})
+
+
+def test_file_upload(session, create_files, add_event_listeners, tracked_events, inline):
+ expected_events = [
+ "input",
+ "change",
+ ]
+
+ files = create_files(["foo", "bar"])
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+ add_event_listeners(element, tracked_events)
+
+ response = element_send_keys(session, element, map_files_to_multiline_text(files))
+ assert_success(response)
+
+ assert_events_equal(session, expected_events)
+
+
+@pytest.mark.parametrize("tag", ["input", "textarea"])
+def test_form_control_send_text(session, add_event_listeners, tracked_events, inline, tag):
+ expected_events = [
+ "focus",
+ "keydown",
+ "keypress",
+ "input",
+ "keyup",
+ "keydown",
+ "keypress",
+ "input",
+ "keyup",
+ "keydown",
+ "keypress",
+ "input",
+ "keyup",
+ ]
+
+ session.url = inline("<%s>" % tag)
+ element = session.find.css(tag, all=False)
+ add_event_listeners(element, tracked_events)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+ assert_events_equal(session, expected_events)
+
+
+@pytest.mark.parametrize("tag", ["input", "textarea"])
+def test_not_blurred(session, inline, tag):
+ session.url = inline("<%s>" % tag)
+ element = session.find.css(tag, all=False)
+
+ response = element_send_keys(session, element, "")
+ assert_success(response)
+ assert_element_has_focus(element)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/file_upload.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/file_upload.py
new file mode 100644
index 0000000000..f62a633c20
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/file_upload.py
@@ -0,0 +1,262 @@
+import pytest
+
+from tests.support.asserts import (assert_element_has_focus,
+ assert_error,
+ assert_files_uploaded,
+ assert_success)
+
+from . import map_files_to_multiline_text
+
+
+def element_send_keys(session, element, text):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id,
+ element_id=element.id),
+ {"text": text})
+
+
+def test_empty_text(session, inline):
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "")
+ assert_error(response, "invalid argument")
+
+
+def test_multiple_files(session, create_files, inline):
+ files = create_files(["foo", "bar"])
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(files))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, files)
+
+
+def test_multiple_files_last_path_not_found(session, create_files, inline):
+ files = create_files(["foo", "bar"])
+ files.append("foo bar")
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(files))
+ assert_error(response, "invalid argument")
+
+ assert_files_uploaded(session, element, [])
+
+
+def test_multiple_files_without_multiple_attribute(session, create_files, inline):
+ files = create_files(["foo", "bar"])
+
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(files))
+ assert_error(response, "invalid argument")
+
+ assert_files_uploaded(session, element, [])
+
+
+def test_multiple_files_send_twice(session, create_files, inline):
+ first_files = create_files(["foo", "bar"])
+ second_files = create_files(["john", "doe"])
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(first_files))
+ assert_success(response)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(second_files))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, first_files + second_files)
+
+
+def test_multiple_files_reset_with_element_clear(session, create_files, inline):
+ first_files = create_files(["foo", "bar"])
+ second_files = create_files(["john", "doe"])
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(first_files))
+ assert_success(response)
+
+ # Reset already uploaded files
+ element.clear()
+ assert_files_uploaded(session, element, [])
+
+ response = element_send_keys(session, element,
+ map_files_to_multiline_text(second_files))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, second_files)
+
+
+def test_single_file(session, create_files, inline):
+ files = create_files(["foo"])
+
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, files)
+
+
+def test_single_file_replaces_without_multiple_attribute(session, create_files, inline):
+ files = create_files(["foo", "bar"])
+
+ session.url = inline("<input type=file>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+
+ response = element_send_keys(session, element, str(files[1]))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, [files[1]])
+
+
+def test_single_file_appends_with_multiple_attribute(session, create_files, inline):
+ files = create_files(["foo", "bar"])
+
+ session.url = inline("<input type=file multiple>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+
+ response = element_send_keys(session, element, str(files[1]))
+ assert_success(response)
+
+ assert_files_uploaded(session, element, files)
+
+
+def test_transparent(session, create_files, inline):
+ files = create_files(["foo"])
+ session.url = inline("""<input type=file style="opacity: 0">""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+ assert_files_uploaded(session, element, files)
+
+
+def test_obscured(session, create_files, inline):
+ files = create_files(["foo"])
+ session.url = inline("""
+ <style>
+ div {
+ position: absolute;
+ width: 100vh;
+ height: 100vh;
+ background: blue;
+ top: 0;
+ left: 0;
+ }
+ </style>
+
+ <input type=file>
+ <div></div>
+ """)
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+ assert_files_uploaded(session, element, files)
+
+
+def test_outside_viewport(session, create_files, inline):
+ files = create_files(["foo"])
+ session.url = inline("""<input type=file style="margin-left: -100vh">""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+ assert_files_uploaded(session, element, files)
+
+
+def test_hidden(session, create_files, inline):
+ files = create_files(["foo"])
+ session.url = inline("<input type=file hidden>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+ assert_files_uploaded(session, element, files)
+
+
+def test_display_none(session, create_files, inline):
+ files = create_files(["foo"])
+ session.url = inline("""<input type=file style="display: none">""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+ assert_files_uploaded(session, element, files)
+
+
+@pytest.mark.capabilities({"strictFileInteractability": False})
+def test_not_focused(session, create_files, inline):
+ files = create_files(["foo"])
+
+ session.url = inline("<input type=file>")
+ body = session.find.css("body", all=False)
+ element = session.find.css("input", all=False)
+ assert_element_has_focus(body)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+ assert_element_has_focus(body)
+
+ assert_files_uploaded(session, element, files)
+
+
+@pytest.mark.capabilities({"strictFileInteractability": True})
+def test_focused(session, create_files, inline):
+ files = create_files(["foo"])
+
+ session.url = inline("<input type=file>")
+ body = session.find.css("body", all=False)
+ element = session.find.css("input", all=False)
+ assert_element_has_focus(body)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_success(response)
+ assert_element_has_focus(element)
+
+ assert_files_uploaded(session, element, files)
+
+
+@pytest.mark.capabilities({"strictFileInteractability": True})
+def test_strict_hidden(session, create_files, inline):
+ files = create_files(["foo"])
+ session.url = inline("<input type=file hidden>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_error(response, "element not interactable")
+
+
+@pytest.mark.capabilities({"strictFileInteractability": True})
+def test_strict_display_none(session, create_files, inline):
+ files = create_files(["foo"])
+ session.url = inline("""<input type=file style="display: none">""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, str(files[0]))
+ assert_error(response, "element not interactable")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/form_controls.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/form_controls.py
new file mode 100644
index 0000000000..364d4c28fa
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/form_controls.py
@@ -0,0 +1,102 @@
+from tests.support.asserts import assert_element_has_focus
+
+
+def element_send_keys(session, element, text):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id,
+ element_id=element.id),
+ {"text": text})
+
+
+def test_input(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+ assert element.property("value") == ""
+
+ element_send_keys(session, element, "foo")
+ assert element.property("value") == "foo"
+ assert_element_has_focus(element)
+
+
+def test_textarea(session, inline):
+ session.url = inline("<textarea>")
+ element = session.find.css("textarea", all=False)
+ assert element.property("value") == ""
+
+ element_send_keys(session, element, "foo")
+ assert element.property("value") == "foo"
+ assert_element_has_focus(element)
+
+
+def test_input_append(session, inline):
+ session.url = inline("<input value=a>")
+ body = session.find.css("body", all=False)
+ assert_element_has_focus(body)
+ element = session.find.css("input", all=False)
+ assert element.property("value") == "a"
+
+ element_send_keys(session, element, "b")
+ assert_element_has_focus(element)
+ assert element.property("value") == "ab"
+
+ element_send_keys(session, element, "c")
+ assert element.property("value") == "abc"
+
+
+def test_textarea_append(session, inline):
+ session.url = inline("<textarea>a</textarea>")
+ body = session.find.css("body", all=False)
+ assert_element_has_focus(body)
+ element = session.find.css("textarea", all=False)
+ assert element.property("value") == "a"
+
+ element_send_keys(session, element, "b")
+ assert_element_has_focus(element)
+ assert element.property("value") == "ab"
+
+ element_send_keys(session, element, "c")
+ assert element.property("value") == "abc"
+
+
+def test_input_insert_when_focused(session, inline):
+ session.url = inline("""<input value=a>
+<script>
+let elem = document.getElementsByTagName("input")[0];
+elem.focus();
+elem.setSelectionRange(0, 0);
+</script>""")
+ element = session.find.css("input", all=False)
+ assert element.property("value") == "a"
+
+ element_send_keys(session, element, "b")
+ assert element.property("value") == "ba"
+
+ element_send_keys(session, element, "c")
+ assert element.property("value") == "bca"
+
+
+def test_textarea_insert_when_focused(session, inline):
+ session.url = inline("""<textarea>a</textarea>
+<script>
+let elem = document.getElementsByTagName("textarea")[0];
+elem.focus();
+elem.setSelectionRange(0, 0);
+</script>""")
+ element = session.find.css("textarea", all=False)
+ assert element.property("value") == "a"
+
+ element_send_keys(session, element, "b")
+ assert element.property("value") == "ba"
+
+ element_send_keys(session, element, "c")
+ assert element.property("value") == "bca"
+
+
+def test_date(session, inline):
+ session.url = inline("<input type=date>")
+ element = session.find.css("input", all=False)
+
+ element_send_keys(session, element, "2000-01-01")
+ assert element.property("value") == "2000-01-01"
+ assert_element_has_focus(element)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/interactability.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/interactability.py
new file mode 100644
index 0000000000..273843fb7b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/interactability.py
@@ -0,0 +1,142 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def element_send_keys(session, element, text):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id,
+ element_id=element.id),
+ {"text": text})
+
+
+def test_body_is_interactable(session, inline):
+ session.url = inline("""
+ <body onkeypress="document.querySelector('input').value += event.key">
+ <input>
+ </body>
+ """)
+
+ element = session.find.css("body", all=False)
+ result = session.find.css("input", all=False)
+
+ # By default body is the active element
+ assert session.active_element == element
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+ assert session.active_element == element
+ assert result.property("value") == "foo"
+
+
+def test_document_element_is_interactable(session, inline):
+ session.url = inline("""
+ <html onkeypress="document.querySelector('input').value += event.key">
+ <input>
+ </html>
+ """)
+
+ body = session.find.css("body", all=False)
+ element = session.find.css(":root", all=False)
+ result = session.find.css("input", all=False)
+
+ # By default body is the active element
+ assert session.active_element == body
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+ assert session.active_element == element
+ assert result.property("value") == "foo"
+
+
+def test_iframe_is_interactable(session, inline, iframe):
+ session.url = inline(iframe("""
+ <body onkeypress="document.querySelector('input').value += event.key">
+ <input>
+ </body>
+ """))
+
+ body = session.find.css("body", all=False)
+ frame = session.find.css("iframe", all=False)
+
+ # By default the body has the focus
+ assert session.active_element == body
+
+ response = element_send_keys(session, frame, "foo")
+ assert_success(response)
+ assert session.active_element == frame
+
+ # Any key events are immediately routed to the nested
+ # browsing context's active document.
+ session.switch_frame(frame)
+ result = session.find.css("input", all=False)
+ assert result.property("value") == "foo"
+
+
+def test_transparent_element(session, inline):
+ session.url = inline("""<input style="opacity: 0">""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+ assert element.property("value") == "foo"
+
+
+def test_readonly_element(session, inline):
+ session.url = inline("<input readonly>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+ assert element.property("value") == ""
+
+
+def test_obscured_element(session, inline):
+ session.url = inline("""
+ <input>
+ <div style="position: relative; top: -3em; height: 5em; background: blue;"></div>
+ """)
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+ assert element.property("value") == "foo"
+
+
+def test_not_a_focusable_element(session, inline):
+ session.url = inline("<div>foo</div>")
+ element = session.find.css("div", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "element not interactable")
+
+
+def test_display_none(session, inline):
+ session.url = inline("""<input style="display: none">""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "element not interactable")
+
+
+def test_visibility_hidden(session, inline):
+ session.url = inline("""<input style="visibility: hidden">""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "element not interactable")
+
+
+def test_hidden(session, inline):
+ session.url = inline("<input hidden>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "element not interactable")
+
+
+def test_disabled(session, inline):
+ session.url = inline("""<input disabled>""")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "element not interactable")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/scroll_into_view.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/scroll_into_view.py
new file mode 100644
index 0000000000..7ccaeaf814
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/scroll_into_view.py
@@ -0,0 +1,40 @@
+from tests.support.asserts import assert_success
+from tests.support.helpers import is_element_in_viewport
+
+
+def element_send_keys(session, element, text):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id,
+ element_id=element.id),
+ {"text": text})
+
+
+def test_element_outside_of_not_scrollable_viewport(session, inline):
+ session.url = inline("<input style=\"position: relative; left: -9999px;\">")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+
+ assert not is_element_in_viewport(session, element)
+
+
+def test_element_outside_of_scrollable_viewport(session, inline):
+ session.url = inline("<input style=\"margin-top: 102vh;\">")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+
+ assert is_element_in_viewport(session, element)
+
+
+def test_contenteditable_element_outside_of_scrollable_viewport(session, inline):
+ session.url = inline("<div contenteditable style=\"margin-top: 102vh;\"></div>")
+ element = session.find.css("div", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+
+ assert is_element_in_viewport(session, element)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/send_keys.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/send_keys.py
new file mode 100644
index 0000000000..7b25d65a1b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/send_keys.py
@@ -0,0 +1,132 @@
+import pytest
+
+from webdriver import WebElement
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def element_send_keys(session, element, text):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id,
+ element_id=element.id),
+ {"text": text})
+
+
+def test_null_parameter_value(session, http, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ path = "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id, element_id=element.id)
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, "foo")
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ element = WebElement(session, "foo")
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "no such window")
+
+ original_handle, element = closed_window
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ element = WebElement(session, "foo")
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = element_send_keys(session, element.shadow_root, "foo")
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("input#text", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "stale element reference")
+
+
+@pytest.mark.parametrize("value", [True, None, 1, [], {}])
+def test_invalid_text_type(session, inline, value):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ response = element_send_keys(session, element, value)
+ assert_error(response, "invalid argument")
+
+
+def test_surrogates(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ text = "🦥🍄"
+ response = element_send_keys(session, element, text)
+ assert_success(response)
+
+ assert element.property("value") == text
diff --git a/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/user_prompts.py
new file mode 100644
index 0000000000..c1046840fa
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/element_send_keys/user_prompts.py
@@ -0,0 +1,123 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def element_send_keys(session, element, text):
+ return session.transport.send(
+ "POST", "/session/{session_id}/element/{element_id}/value".format(
+ session_id=session.session_id,
+ element_id=element.id),
+ {"text": text})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_send_keys(session, element, "foo")
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert element.property("value") == "foo"
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert element.property("value") == ""
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input type=text>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = element_send_keys(session, element, "foo")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert element.property("value") == ""
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/__init__.py
new file mode 100644
index 0000000000..9cd37ecdca
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/__init__.py
@@ -0,0 +1,16 @@
+import webdriver.protocol as protocol
+
+
+def execute_async_script(session, script, args=None):
+ if args is None:
+ args = []
+ body = {"script": script, "args": args}
+
+ return session.transport.send(
+ "POST",
+ "/session/{session_id}/execute/async".format(**vars(session)),
+ body,
+ encoder=protocol.Encoder,
+ decoder=protocol.Decoder,
+ session=session,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/arguments.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/arguments.py
new file mode 100644
index 0000000000..81b30de267
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/arguments.py
@@ -0,0 +1,205 @@
+import pytest
+
+from webdriver.client import ShadowRoot, WebElement, WebFrame, WebWindow
+
+from tests.support.asserts import assert_error, assert_success
+from . import execute_async_script
+
+
+def test_null(session):
+ value = None
+ result = execute_async_script(session, """
+ arguments[1]([arguments[0] === null, arguments[0]])
+ """, args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] is True
+ assert actual[1] == value
+
+
+@pytest.mark.parametrize("value, expected_type", [
+ (True, "boolean"),
+ (42, "number"),
+ ("foo", "string"),
+], ids=["boolean", "number", "string"])
+def test_primitives(session, value, expected_type):
+ result = execute_async_script(session, """
+ arguments[1]([typeof arguments[0], arguments[0]])
+ """, args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] == expected_type
+ assert actual[1] == value
+
+
+def test_collection(session):
+ value = [1, 2, 3]
+ result = execute_async_script(session, """
+ arguments[1]([Array.isArray(arguments[0]), arguments[0]])
+ """, args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] is True
+ assert actual[1] == value
+
+
+def test_object(session):
+ value = {"foo": "bar", "cheese": 23}
+ result = execute_async_script(session, """
+ arguments[1]([typeof arguments[0], arguments[0]])
+ """, args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] == "object"
+ assert actual[1] == value
+
+
+def test_no_such_element_with_unknown_id(session):
+ element = WebElement(session, "foo")
+
+ result = execute_async_script(session, """
+ arguments[1](true);
+ """, args=[element])
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ result = execute_async_script(session, """
+ arguments[1](true);
+ """, args=[element])
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ result = execute_async_script(session, """
+ arguments[1](true);
+ """, args=[element])
+ assert_error(result, "no such element")
+
+
+def test_no_such_shadow_root_with_unknown_id(session):
+ shadow_root = ShadowRoot(session, "foo")
+
+ result = execute_async_script(session, """
+ arguments[1](true);
+ """, args=[shadow_root])
+ assert_error(result, "no such shadow root")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_shadow_root_from_other_window_handle(session, get_test_page, closed):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ result = execute_async_script(session, """
+ arguments[1](true);
+ """, args=[shadow_root])
+ assert_error(result, "no such shadow root")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_shadow_root_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ result = execute_async_script(session, """
+ arguments[1](true);
+ """, args=[shadow_root])
+ assert_error(result, "no such shadow root")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = execute_async_script(session, "arguments[1](1);", args=[element])
+ assert_error(result, "stale element reference")
+
+
+@pytest.mark.parametrize("type", [WebFrame, WebWindow], ids=["frame", "window"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+def test_invalid_argument_for_window_with_invalid_type(session, type, value):
+ reference = type(session, value)
+
+ result = execute_async_script(session, "arguments[1](true)", args=(reference,))
+ assert_error(result, "invalid argument")
+
+
+def test_no_such_window_for_window_with_invalid_value(session, get_test_page):
+ session.url = get_test_page()
+
+ result = execute_async_script(session, "arguments[0]([window, window.frames[0]]);")
+ [window, frame] = assert_success(result)
+
+ assert isinstance(window, WebWindow)
+ assert isinstance(frame, WebFrame)
+
+ window_reference = WebWindow(session, frame.id)
+ frame_reference = WebFrame(session, window.id)
+
+ for reference in [window_reference, frame_reference]:
+ result = execute_async_script(session, "arguments[1](true)", args=(reference,))
+ assert_error(result, "no such window")
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("window.frames[0]", WebFrame),
+ ("document.querySelector('div')", WebElement),
+ ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
+ ("window", WebWindow)
+], ids=["frame", "node", "shadow-root", "window"])
+def test_element_reference(session, get_test_page, expression, expected_type):
+ session.url = get_test_page(as_frame=False)
+
+ result = execute_async_script(session, f"arguments[0]({expression})")
+ reference = assert_success(result)
+ assert isinstance(reference, expected_type)
+
+ result = execute_async_script(session, f"""
+ let resolve = arguments[1];
+ resolve(arguments[0] == {expression})
+ """, [reference])
+ assert_success(result, True)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/collections.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/collections.py
new file mode 100644
index 0000000000..2b57120a4c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/collections.py
@@ -0,0 +1,165 @@
+import os
+
+from tests.support.asserts import assert_same_element, assert_success
+from . import execute_async_script
+
+
+def test_arguments(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ function func() {
+ return arguments;
+ }
+ resolve(func("foo", "bar"));
+ """)
+ assert_success(response, [u"foo", u"bar"])
+
+
+def test_array(session):
+ response = execute_async_script(
+ session, "arguments[0]([1, 2])")
+ assert_success(response, [1, 2])
+
+
+def test_array_in_array(session):
+ response = execute_async_script(
+ session, "const arr = [1]; arguments[0]([arr, arr])")
+ assert_success(response, [[1], [1]])
+
+
+def test_dom_token_list(session, inline):
+ session.url = inline("""<div class="no cheese">foo</div>""")
+ element = session.find.css("div", all=False)
+
+ response = execute_async_script(
+ session, "arguments[1](arguments[0].classList)", args=[element])
+ value = assert_success(response)
+
+ assert value == ["no", "cheese"]
+
+
+def test_file_list(session, tmpdir, inline):
+ files = [tmpdir.join("foo.txt"), tmpdir.join("bar.txt")]
+
+ session.url = inline("<input type=file multiple>")
+ upload = session.find.css("input", all=False)
+ for file in files:
+ file.write("morn morn")
+ upload.send_keys(str(file))
+
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(document.querySelector('input').files);
+ """)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == len(files)
+ for expected, actual in zip(files, value):
+ assert isinstance(actual, dict)
+ assert "name" in actual
+ assert isinstance(actual["name"], str)
+ assert os.path.basename(str(expected)) == actual["name"]
+
+
+def test_html_all_collection(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+ """)
+ html = session.find.css("html", all=False)
+ head = session.find.css("head", all=False)
+ meta = session.find.css("meta", all=False)
+ body = session.find.css("body", all=False)
+ ps = session.find.css("p")
+
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(document.all);
+ """)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ # <html>, <head>, <meta>, <body>, <p>, <p>
+ assert len(value) == 6
+
+ assert_same_element(session, html, value[0])
+ assert_same_element(session, head, value[1])
+ assert_same_element(session, meta, value[2])
+ assert_same_element(session, body, value[3])
+ assert_same_element(session, ps[0], value[4])
+ assert_same_element(session, ps[1], value[5])
+
+
+def test_html_collection(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+ """)
+ ps = session.find.css("p")
+
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(document.getElementsByTagName('p'));
+ """)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(ps, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_html_form_controls_collection(session, inline):
+ session.url = inline("""
+ <form>
+ <input>
+ <input>
+ </form>
+ """)
+ inputs = session.find.css("input")
+
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(document.forms[0].elements);
+ """)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(inputs, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_html_options_collection(session, inline):
+ session.url = inline("""
+ <select>
+ <option>
+ <option>
+ </select>
+ """)
+ options = session.find.css("option")
+
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(document.querySelector('select').options);
+ """)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(options, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_node_list(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+ """)
+ ps = session.find.css("p")
+
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(document.querySelectorAll('p'));
+ """)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(ps, value):
+ assert_same_element(session, expected, actual)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/cyclic.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/cyclic.py
new file mode 100644
index 0000000000..ff536f3477
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/cyclic.py
@@ -0,0 +1,78 @@
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+from . import execute_async_script
+
+
+def test_collection_self_reference(session):
+ response = execute_async_script(session, """
+ let arr = [];
+ arr.push(arr);
+ arguments[0](arr);
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_element_self_reference(session, inline):
+ session.url = inline("<div></div>")
+ div = session.find.css("div", all=False)
+
+ response = execute_async_script(session, """
+ let div = document.querySelector("div");
+ div.reference = div;
+ arguments[0](div);
+ """)
+ value = assert_success(response)
+ assert_same_element(session, value, div)
+
+
+def test_object_self_reference(session):
+ response = execute_async_script(session, """
+ let obj = {};
+ obj.reference = obj;
+ arguments[0](obj);
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_collection_self_reference_in_object(session):
+ response = execute_async_script(session, """
+ let arr = [];
+ arr.push(arr);
+ arguments[0]({'value': arr});
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_object_self_reference_in_collection(session):
+ response = execute_async_script(session, """
+ let obj = {};
+ obj.reference = obj;
+ arguments[0]([obj]);
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_element_self_reference_in_collection(session, inline):
+ session.url = inline("<div></div>")
+ divs = session.find.css("div")
+
+ response = execute_async_script(session, """
+ let div = document.querySelector("div");
+ div.reference = div;
+ arguments[0]([div]);
+ """)
+ value = assert_success(response)
+ for expected, actual in zip(divs, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_element_self_reference_in_object(session, inline):
+ session.url = inline("<div></div>")
+ div = session.find.css("div", all=False)
+
+ response = execute_async_script(session, """
+ let div = document.querySelector("div");
+ div.reference = div;
+ arguments[0]({foo: div});
+ """)
+ value = assert_success(response)
+ assert_same_element(session, div, value["foo"])
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/execute_async.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/execute_async.py
new file mode 100644
index 0000000000..cdef3230cb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/execute_async.py
@@ -0,0 +1,79 @@
+import pytest
+
+from webdriver.error import NoSuchAlertException
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.sync import Poll
+from . import execute_async_script
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/execute/async".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = execute_async_script(session, "arguments[0](1);")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = execute_async_script(session, "arguments[0](1);")
+ assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize("expression, expected", [
+ ("null", None),
+ ("undefined", None),
+ ("true", True),
+ ("false", False),
+ ("23", 23),
+ ("'foo'", "foo"),
+ (
+ # Compute value in the runtime to reduce the potential for
+ # interference from encoding literal bytes or escape sequences in
+ # Python and HTTP.
+ "String.fromCharCode(0)",
+ "\x00"
+ )
+])
+def test_primitive_serialization(session, expression, expected):
+ response = execute_async_script(session, "arguments[0]({});".format(expression))
+ value = assert_success(response)
+ assert value == expected
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_abort_by_user_prompt(session, dialog_type):
+ response = execute_async_script(
+ session,
+ "window.{}('Hello'); arguments[0](1);".format(dialog_type))
+ assert_success(response, None)
+
+ session.alert.accept()
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_abort_by_user_prompt_twice(session, dialog_type):
+ response = execute_async_script(
+ session,
+ "window.{0}('Hello'); window.{0}('Bye'); arguments[0](1);".format(dialog_type))
+ assert_success(response, None)
+
+ session.alert.accept()
+
+ # The first alert has been accepted by the user prompt handler, the second
+ # alert will still be opened because the current step isn't aborted.
+ wait = Poll(
+ session,
+ timeout=5,
+ message="Second alert has not been opened",
+ ignored_exceptions=NoSuchAlertException
+ )
+ text = wait.until(lambda s: s.alert.text)
+
+ assert text == "Bye"
+
+ session.alert.accept()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/node.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/node.py
new file mode 100644
index 0000000000..2f1bf75e83
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/node.py
@@ -0,0 +1,86 @@
+import pytest
+
+from webdriver.client import ShadowRoot, WebElement
+
+from tests.support.asserts import assert_error, assert_success
+from . import execute_async_script
+
+
+PAGE_DATA = """
+ <div id="deep"><p><span></span></p><br/></div>
+ <div id="text-node"><p></p>Lorem</div>
+ <br/>
+ <svg id="foo"></svg>
+ <div id="comment"><!-- Comment --></div>
+ <script>
+ var svg = document.querySelector("svg");
+ svg.setAttributeNS("http://www.w3.org/2000/svg", "svg:foo", "bar");
+ </script>
+"""
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_detached_shadow_root(session, get_test_page, as_frame):
+ session.url = get_test_page(as_frame)
+
+ if as_frame:
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("custom-element", all=False)
+
+ # Retrieve shadow root to add it to the node cache
+ shadow_root = element.shadow_root
+
+ result = execute_async_script(session, """
+ const [elem, shadowRoot, resolve] = arguments;
+ elem.remove();
+ resolve(shadowRoot);
+ """, args=[element, shadow_root])
+ assert_error(result, "detached shadow root")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element(session, get_test_page, as_frame):
+ session.url = get_test_page(as_frame=as_frame)
+
+ if as_frame:
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ result = execute_async_script(session, """
+ const [elem, resolve] = arguments;
+ elem.remove();
+ resolve(elem);
+ """, args=[element])
+ assert_error(result, "stale element reference")
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("document.querySelector('div')", WebElement),
+ ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
+], ids=["element", "shadow-root"])
+def test_element_reference(session, get_test_page, expression, expected_type):
+ session.url = get_test_page()
+
+ result = execute_async_script(session, f"arguments[0]({expression})")
+ reference = assert_success(result)
+ assert isinstance(reference, expected_type)
+
+
+@pytest.mark.parametrize("expression", [
+ (""" document.querySelector("svg").attributes[0] """),
+ (""" document.querySelector("div#text-node").childNodes[1] """),
+ (""" document.querySelector("foo").childNodes[1] """),
+ (""" document.createProcessingInstruction("xml-stylesheet", "href='foo.css'") """),
+ (""" document.querySelector("div#comment").childNodes[0] """),
+ (""" document"""),
+ (""" document.doctype"""),
+], ids=["attribute", "text", "cdata", "processing_instruction", "comment", "document", "doctype"])
+def test_not_supported_nodes(session, inline, expression):
+ session.url = inline(PAGE_DATA)
+
+ result = execute_async_script(session, f"arguments[0]({expression})")
+ assert_error(result, "javascript error")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/objects.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/objects.py
new file mode 100644
index 0000000000..2957429b01
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/objects.py
@@ -0,0 +1,49 @@
+from tests.support.asserts import assert_error, assert_success
+from . import execute_async_script
+
+
+def test_object(session):
+ response = execute_async_script(session, """
+ arguments[0]({
+ foo: 23,
+ bar: true,
+ });
+ """)
+ value = assert_success(response)
+ assert value == {"foo": 23, "bar": True}
+
+
+def test_nested_object(session):
+ response = execute_async_script(session, """
+ arguments[0]({
+ foo: {
+ cheese: 23,
+ },
+ bar: true,
+ });
+ """)
+ value = assert_success(response)
+ assert value == {"foo": {"cheese": 23}, "bar": True}
+
+
+def test_object_to_json(session):
+ response = execute_async_script(session, """
+ arguments[0]({
+ toJSON() {
+ return ["foo", "bar"];
+ }
+ });
+ """)
+ value = assert_success(response)
+ assert value == ["foo", "bar"]
+
+
+def test_object_to_json_exception(session):
+ response = execute_async_script(session, """
+ arguments[0]({
+ toJSON() {
+ throw Error("fail");
+ }
+ });
+ """)
+ assert_error(response, "javascript error")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/promise.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/promise.py
new file mode 100644
index 0000000000..d726d0d712
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/promise.py
@@ -0,0 +1,118 @@
+from tests.support.asserts import assert_error, assert_success
+from . import execute_async_script
+
+
+def test_promise_resolve(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(Promise.resolve('foobar'));
+ """)
+ assert_success(response, "foobar")
+
+
+def test_promise_resolve_delayed(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ let promise = new Promise(
+ (resolve) => setTimeout(
+ () => resolve('foobar'),
+ 50
+ )
+ );
+ resolve(promise);
+ """)
+ assert_success(response, "foobar")
+
+
+def test_promise_all_resolve(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ let promise = Promise.all([
+ Promise.resolve(1),
+ Promise.resolve(2)
+ ]);
+ resolve(promise);
+ """)
+ assert_success(response, [1, 2])
+
+
+def test_await_promise_resolve(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ let res = await Promise.resolve('foobar');
+ resolve(res);
+ """)
+ assert_success(response, "foobar")
+
+
+def test_promise_resolve_timeout(session):
+ session.timeouts.script = .1
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ let promise = new Promise(
+ (resolve) => setTimeout(
+ () => resolve(),
+ 1000
+ )
+ );
+ resolve(promise);
+ """)
+ assert_error(response, "script timeout")
+
+
+def test_promise_reject(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ resolve(Promise.reject(new Error('my error')));
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_promise_reject_delayed(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ let promise = new Promise(
+ (resolve, reject) => setTimeout(
+ () => reject(new Error('my error')),
+ 50
+ )
+ );
+ resolve(promise);
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_promise_all_reject(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ let promise = Promise.all([
+ Promise.resolve(1),
+ Promise.reject(new Error('error'))
+ ]);
+ resolve(promise);
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_await_promise_reject(session):
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ await Promise.reject(new Error('my error'));
+ resolve('foo');
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_promise_reject_timeout(session):
+ session.timeouts.script = .1
+ response = execute_async_script(session, """
+ let resolve = arguments[0];
+ let promise = new Promise(
+ (resolve, reject) => setTimeout(
+ () => reject(new Error('my error')),
+ 1000
+ )
+ );
+ resolve(promise);
+ """)
+ assert_error(response, "script timeout")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/properties.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/properties.py
new file mode 100644
index 0000000000..b9592e7edd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/properties.py
@@ -0,0 +1,64 @@
+from tests.support.asserts import assert_same_element, assert_success
+from . import execute_async_script
+
+
+def test_content_attribute(session, inline):
+ session.url = inline("<input value=foobar>")
+ response = execute_async_script(session, """
+ const resolve = arguments[0];
+ const input = document.querySelector("input");
+ resolve(input.value);
+ """)
+ assert_success(response, "foobar")
+
+
+def test_idl_attribute(session, inline):
+ session.url = inline("""
+ <input>
+ <script>
+ const input = document.querySelector("input");
+ input.value = "foobar";
+ </script>
+ """)
+ response = execute_async_script(session, """
+ const resolve = arguments[0];
+ const input = document.querySelector("input");
+ resolve(input.value);
+ """)
+ assert_success(response, "foobar")
+
+
+def test_idl_attribute_element(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+
+ <script>
+ const elements = document.querySelectorAll("p");
+ let foo = elements[0];
+ let bar = elements[1];
+ foo.bar = bar;
+ </script>
+ """)
+ _foo, bar = session.find.css("p")
+ response = execute_async_script(session, """
+ const resolve = arguments[0];
+ const foo = document.querySelector("p");
+ resolve(foo.bar);
+ """)
+ value = assert_success(response)
+ assert_same_element(session, bar, value)
+
+
+def test_script_defining_property(session, inline):
+ session.url = inline("<input>")
+ session.execute_script("""
+ const input = document.querySelector("input");
+ input.foobar = "foobar";
+ """)
+ response = execute_async_script(session, """
+ const resolve = arguments[0];
+ const input = document.querySelector("input");
+ resolve(input.foobar);
+ """)
+ assert_success(response, "foobar")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/user_prompts.py
new file mode 100644
index 0000000000..5243b372e5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/user_prompts.py
@@ -0,0 +1,195 @@
+# META: timeout=long
+
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+from tests.support.sync import Poll
+from . import execute_async_script
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+ page_target = url("/webdriver/tests/support/html/default.html")
+
+ session.url = page_beforeunload
+
+ element = session.find.css("input", all=False)
+ element.send_keys("bar")
+
+ response = execute_async_script(
+ session, """
+ const [url, resolve] = arguments;
+ window.location.href = url;
+ resolve();
+ """, args=(page_target,))
+ assert_success(response)
+
+ wait = Poll(
+ session,
+ timeout=5,
+ message="Target page did not load")
+ wait.until(lambda s: s.url == page_target)
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = execute_async_script(session, "window.result = 1; arguments[0](1);")
+ assert_success(response, 1)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.execute_script("return window.result;") == 1
+
+ return check_user_prompt_closed_without_exception
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = execute_async_script(session, "window.result = 1; arguments[0](1);")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.execute_script("return window.result;") is None
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = execute_async_script(session, "window.result = 1; arguments[0](1);")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.execute_script("return window.result;") is None
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/window.py b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/window.py
new file mode 100644
index 0000000000..f79bfdf49d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_async_script/window.py
@@ -0,0 +1,33 @@
+import pytest
+
+from webdriver.client import WebFrame, WebWindow
+
+from tests.support.asserts import assert_success
+from . import execute_async_script
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("window.frames[0]", WebFrame),
+ ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference(session, get_test_page, expression, expected_type):
+ session.url = get_test_page()
+
+ result = execute_async_script(session, f"arguments[0]({expression})")
+ reference = assert_success(result)
+
+ assert isinstance(reference, expected_type)
+
+ if isinstance(reference, WebWindow):
+ assert reference.id in session.handles
+ else:
+ assert reference.id not in session.handles
+
+
+def test_window_open(session):
+ result = execute_async_script(
+ session, "window.foo = window.open(); arguments[0](window.foo);")
+ reference = assert_success(result)
+
+ assert isinstance(reference, WebWindow)
+ assert reference.id in session.handles
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/__init__.py
new file mode 100644
index 0000000000..1ab36eb054
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/__init__.py
@@ -0,0 +1,16 @@
+import webdriver.protocol as protocol
+
+
+def execute_script(session, script, args=None):
+ if args is None:
+ args = []
+ body = {"script": script, "args": args}
+
+ return session.transport.send(
+ "POST",
+ "/session/{session_id}/execute/sync".format(**vars(session)),
+ body,
+ encoder=protocol.Encoder,
+ decoder=protocol.Decoder,
+ session=session,
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/arguments.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/arguments.py
new file mode 100644
index 0000000000..ab5c5234ba
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/arguments.py
@@ -0,0 +1,190 @@
+import pytest
+
+from webdriver.client import ShadowRoot, WebElement, WebFrame, WebWindow
+
+from tests.support.asserts import assert_error, assert_success
+from . import execute_script
+
+
+def test_null(session):
+ value = None
+ result = execute_script(session, "return [arguments[0] === null, arguments[0]]", args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] is True
+ assert actual[1] == value
+
+
+@pytest.mark.parametrize("value, expected_type", [
+ (True, "boolean"),
+ (42, "number"),
+ ("foo", "string"),
+], ids=["boolean", "number", "string"])
+def test_primitives(session, value, expected_type):
+ result = execute_script(session, "return [typeof arguments[0], arguments[0]]", args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] == expected_type
+ assert actual[1] == value
+
+
+def test_collection(session):
+ value = [1, 2, 3]
+ result = execute_script(session, "return [Array.isArray(arguments[0]), arguments[0]]", args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] is True
+ assert actual[1] == value
+
+
+def test_object(session):
+ value = {"foo": "bar", "cheese": 23}
+ result = execute_script(session, "return [typeof arguments[0], arguments[0]]", args=[value])
+ actual = assert_success(result)
+
+ assert actual[0] == "object"
+ assert actual[1] == value
+
+
+def test_no_such_element_with_unknown_id(session):
+ element = WebElement(session, "foo")
+
+ result = execute_script(session, "return true;", args=[element])
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ result = execute_script(session, "return true;", args=[element])
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ result = execute_script(session, "return true;", args=[element])
+ assert_error(result, "no such element")
+
+
+def test_no_such_shadow_root_with_unknown_id(session):
+ shadow_root = ShadowRoot(session, "foo")
+
+ result = execute_script(session, "return true;", args=[shadow_root])
+ assert_error(result, "no such shadow root")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_shadow_root_from_other_window_handle(session, get_test_page, closed):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ result = execute_script(session, "return true;", args=[shadow_root])
+ assert_error(result, "no such shadow root")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_shadow_root_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ session.switch_frame("parent")
+
+ if closed:
+ execute_script(session, "arguments[0].remove();", args=[frame])
+
+ result = execute_script(session, "return true;", args=[shadow_root])
+ assert_error(result, "no such shadow root")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_detached_shadow_root_reference(session, stale_element, as_frame):
+ shadow_root = stale_element("custom-element", as_frame=as_frame, want_shadow_root=True)
+
+ result = execute_script(session, "return 1;", args=[shadow_root])
+ assert_error(result, "detached shadow root")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = execute_script(session, "return 1;", args=[element])
+ assert_error(result, "stale element reference")
+
+
+@pytest.mark.parametrize("type", [WebFrame, WebWindow], ids=["frame", "window"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+def test_invalid_argument_for_window_with_invalid_type(session, type, value):
+ reference = type(session, value)
+
+ result = execute_script(session, "return true", args=(reference,))
+ assert_error(result, "invalid argument")
+
+
+def test_no_such_window_for_window_with_invalid_value(session, get_test_page):
+ session.url = get_test_page()
+
+ result = execute_script(session, "return [window, window.frames[0]];")
+ [window, frame] = assert_success(result)
+
+ assert isinstance(window, WebWindow)
+ assert isinstance(frame, WebFrame)
+
+ window_reference = WebWindow(session, frame.id)
+ frame_reference = WebFrame(session, window.id)
+
+ for reference in [window_reference, frame_reference]:
+ result = execute_script(session, "return true", args=(reference,))
+ assert_error(result, "no such window")
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("window.frames[0]", WebFrame),
+ ("document.querySelector('div')", WebElement),
+ ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
+ ("window", WebWindow)
+], ids=["frame", "node", "shadow-root", "window"])
+def test_element_reference(session, get_test_page, expression, expected_type):
+ session.url = get_test_page(as_frame=False)
+
+ result = execute_script(session, f"return {expression}")
+ reference = assert_success(result)
+ assert isinstance(reference, expected_type)
+
+ result = execute_script(session, f"return arguments[0] == {expression}", [reference])
+ assert_success(result, True)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/collections.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/collections.py
new file mode 100644
index 0000000000..939eb8916f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/collections.py
@@ -0,0 +1,143 @@
+import os
+
+from tests.support.asserts import assert_same_element, assert_success
+from . import execute_script
+
+
+def test_arguments(session):
+ response = execute_script(session, """
+ function func() {
+ return arguments;
+ }
+ return func("foo", "bar");
+ """)
+ assert_success(response, [u"foo", u"bar"])
+
+
+def test_array(session):
+ response = execute_script(session, "return [1, 2]")
+ assert_success(response, [1, 2])
+
+
+def test_array_in_array(session):
+ response = execute_script(session, "const arr = [1]; return [arr, arr]")
+ assert_success(response, [[1], [1]])
+
+
+def test_dom_token_list(session, inline):
+ session.url = inline("""<div class="no cheese">foo</div>""")
+ element = session.find.css("div", all=False)
+
+ response = execute_script(session, "return arguments[0].classList", args=[element])
+ value = assert_success(response)
+
+ assert value == ["no", "cheese"]
+
+
+def test_file_list(session, tmpdir, inline):
+ files = [tmpdir.join("foo.txt"), tmpdir.join("bar.txt")]
+
+ session.url = inline("<input type=file multiple>")
+ upload = session.find.css("input", all=False)
+ for file in files:
+ file.write("morn morn")
+ upload.send_keys(str(file))
+
+ response = execute_script(session, "return document.querySelector('input').files")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == len(files)
+ for expected, actual in zip(files, value):
+ assert isinstance(actual, dict)
+ assert "name" in actual
+ assert isinstance(actual["name"], str)
+ assert os.path.basename(str(expected)) == actual["name"]
+
+
+def test_html_all_collection(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+ """)
+ html = session.find.css("html", all=False)
+ head = session.find.css("head", all=False)
+ meta = session.find.css("meta", all=False)
+ body = session.find.css("body", all=False)
+ ps = session.find.css("p")
+
+ response = execute_script(session, "return document.all")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ # <html>, <head>, <meta>, <body>, <p>, <p>
+ assert len(value) == 6
+
+ assert_same_element(session, html, value[0])
+ assert_same_element(session, head, value[1])
+ assert_same_element(session, meta, value[2])
+ assert_same_element(session, body, value[3])
+ assert_same_element(session, ps[0], value[4])
+ assert_same_element(session, ps[1], value[5])
+
+
+def test_html_collection(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+ """)
+ ps = session.find.css("p")
+
+ response = execute_script(session, "return document.getElementsByTagName('p')")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(ps, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_html_form_controls_collection(session, inline):
+ session.url = inline("""
+ <form>
+ <input>
+ <input>
+ </form>
+ """)
+ inputs = session.find.css("input")
+
+ response = execute_script(session, "return document.forms[0].elements")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(inputs, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_html_options_collection(session, inline):
+ session.url = inline("""
+ <select>
+ <option>
+ <option>
+ </select>
+ """)
+ options = session.find.css("option")
+
+ response = execute_script(session, "return document.querySelector('select').options")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(options, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_node_list(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+ """)
+ ps = session.find.css("p")
+
+ response = execute_script(session, "return document.querySelectorAll('p')")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 2
+ for expected, actual in zip(ps, value):
+ assert_same_element(session, expected, actual)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/cyclic.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/cyclic.py
new file mode 100644
index 0000000000..29db2f27e6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/cyclic.py
@@ -0,0 +1,78 @@
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+from . import execute_script
+
+
+def test_collection_self_reference(session):
+ response = execute_script(session, """
+ let arr = [];
+ arr.push(arr);
+ return arr;
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_element_self_reference(session, inline):
+ session.url = inline("<div></div>")
+ div = session.find.css("div", all=False)
+
+ response = execute_script(session, """
+ let div = document.querySelector("div");
+ div.reference = div;
+ return div;
+ """)
+ value = assert_success(response)
+ assert_same_element(session, value, div)
+
+
+def test_object_self_reference(session):
+ response = execute_script(session, """
+ let obj = {};
+ obj.reference = obj;
+ return obj;
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_collection_self_reference_in_object(session):
+ response = execute_script(session, """
+ let arr = [];
+ arr.push(arr);
+ return {'value': arr};
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_object_self_reference_in_collection(session):
+ response = execute_script(session, """
+ let obj = {};
+ obj.reference = obj;
+ return [obj];
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_element_self_reference_in_collection(session, inline):
+ session.url = inline("<div></div>")
+ divs = session.find.css("div")
+
+ response = execute_script(session, """
+ let div = document.querySelector("div");
+ div.reference = div;
+ return [div];
+ """)
+ value = assert_success(response)
+ for expected, actual in zip(divs, value):
+ assert_same_element(session, expected, actual)
+
+
+def test_element_self_reference_in_object(session, inline):
+ session.url = inline("<div></div>")
+ div = session.find.css("div", all=False)
+
+ response = execute_script(session, """
+ let div = document.querySelector("div");
+ div.reference = div;
+ return {foo: div};
+ """)
+ value = assert_success(response)
+ assert_same_element(session, div, value["foo"])
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/execute.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/execute.py
new file mode 100644
index 0000000000..9c3c699bd2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/execute.py
@@ -0,0 +1,114 @@
+import pytest
+
+from webdriver.error import NoSuchAlertException
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.sync import Poll
+from . import execute_script
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/execute/sync".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = execute_script(session, "return 1;")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = execute_script(session, "return 1;")
+ assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize("expression, expected", [
+ ("null", None),
+ ("undefined", None),
+ ("true", True),
+ ("false", False),
+ ("23", 23),
+ ("'foo'", "foo"),
+ (
+ # Compute value in the runtime to reduce the potential for
+ # interference from encoding literal bytes or escape sequences in
+ # Python and HTTP.
+ "String.fromCharCode(0)",
+ "\x00"
+ )
+])
+def test_primitive_serialization(session, expression, expected):
+ response = execute_script(session, "return {};".format(expression))
+ value = assert_success(response)
+ assert value == expected
+
+
+def test_opening_new_window_keeps_current_window_handle(session, inline):
+ original_handle = session.window_handle
+ original_handles = session.handles
+
+ url = inline("""<a href="javascript:window.open();">open window</a>""")
+ session.url = url
+ session.find.css("a", all=False).click()
+ wait = Poll(
+ session,
+ timeout=5,
+ message="No new window has been opened")
+ new_handles = wait.until(lambda s: set(s.handles) - set(original_handles))
+
+ assert len(new_handles) == 1
+ assert session.window_handle == original_handle
+ assert session.url == url
+
+
+def test_ending_comment(session):
+ response = execute_script(session, "return 1; // foo")
+ assert_success(response, 1)
+
+
+def test_override_listeners(session, inline):
+ session.url = inline("""
+<script>
+called = [];
+window.addEventListener = () => {called.push("Internal addEventListener")}
+window.removeEventListener = () => {called.push("Internal removeEventListener")}
+</script>
+})""")
+ response = execute_script(session, "return !window.onunload")
+ assert_success(response, True)
+ response = execute_script(session, "return called")
+ assert_success(response, [])
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_abort_by_user_prompt(session, dialog_type):
+ response = execute_script(
+ session, "window.{}('Hello'); return 1;".format(dialog_type))
+ assert_success(response, None)
+
+ session.alert.accept()
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_abort_by_user_prompt_twice(session, dialog_type):
+ response = execute_script(
+ session, "window.{0}('Hello'); window.{0}('Bye'); return 1;".format(dialog_type))
+ assert_success(response, None)
+
+ session.alert.accept()
+
+ # The first alert has been accepted by the user prompt handler, the second
+ # alert will still be opened because the current step isn't aborted.
+ wait = Poll(
+ session,
+ timeout=5,
+ message="Second alert has not been opened",
+ ignored_exceptions=NoSuchAlertException
+ )
+ text = wait.until(lambda s: s.alert.text)
+
+ assert text == "Bye"
+
+ session.alert.accept()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/node.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/node.py
new file mode 100644
index 0000000000..61cf3463dc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/node.py
@@ -0,0 +1,85 @@
+import pytest
+
+from webdriver.client import WebElement, ShadowRoot
+from tests.support.asserts import assert_error, assert_success
+from . import execute_script
+
+
+PAGE_DATA = """
+ <div id="deep"><p><span></span></p><br/></div>
+ <div id="text-node"><p></p>Lorem</div>
+ <br/>
+ <svg id="foo"></svg>
+ <div id="comment"><!-- Comment --></div>
+ <script>
+ var svg = document.querySelector("svg");
+ svg.setAttributeNS("http://www.w3.org/2000/svg", "svg:foo", "bar");
+ </script>
+"""
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_detached_shadow_root(session, get_test_page, as_frame):
+ session.url = get_test_page(as_frame)
+
+ if as_frame:
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("custom-element", all=False)
+
+ # Retrieve shadow root to add it to the node cache
+ shadow_root = element.shadow_root
+
+ result = execute_script(session, """
+ const [elem, shadowRoot] = arguments;
+ elem.remove();
+ return shadowRoot;
+ """, args=[element, shadow_root])
+ assert_error(result, "detached shadow root")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element(session, get_test_page, as_frame):
+ session.url = get_test_page(as_frame)
+
+ if as_frame:
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ result = execute_script(session, """
+ const elem = arguments[0];
+ elem.remove();
+ return elem;
+ """, args=[element])
+ assert_error(result, "stale element reference")
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("document.querySelector('div')", WebElement),
+ ("document.querySelector('custom-element').shadowRoot", ShadowRoot),
+], ids=["element", "shadow-root"])
+def test_web_reference(session, get_test_page, expression, expected_type):
+ session.url = get_test_page()
+
+ result = execute_script(session, f"return {expression}")
+ reference = assert_success(result)
+ assert isinstance(reference, expected_type)
+
+
+@pytest.mark.parametrize("expression", [
+ (""" document.querySelector("svg").attributes[0] """),
+ (""" document.querySelector("div#text-node").childNodes[1] """),
+ (""" document.querySelector("foo").childNodes[1] """),
+ (""" document.createProcessingInstruction("xml-stylesheet", "href='foo.css'") """),
+ (""" document.querySelector("div#comment").childNodes[0] """),
+ (""" document"""),
+ (""" document.doctype"""),
+], ids=["attribute", "text", "cdata", "processing_instruction", "comment", "document", "doctype"])
+def test_not_supported_nodes(session, inline, expression):
+ session.url = inline(PAGE_DATA)
+
+ result = execute_script(session, f"return {expression}")
+ assert_error(result, "javascript error")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/objects.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/objects.py
new file mode 100644
index 0000000000..6447bce079
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/objects.py
@@ -0,0 +1,49 @@
+from tests.support.asserts import assert_error, assert_success
+from . import execute_script
+
+
+def test_object(session):
+ response = execute_script(session, """
+ return {
+ foo: 23,
+ bar: true,
+ };
+ """)
+ value = assert_success(response)
+ assert value == {"foo": 23, "bar": True}
+
+
+def test_nested_object(session):
+ response = execute_script(session, """
+ return {
+ foo: {
+ cheese: 23,
+ },
+ bar: true,
+ };
+ """)
+ value = assert_success(response)
+ assert value == {"foo": {"cheese": 23}, "bar": True}
+
+
+def test_object_to_json(session):
+ response = execute_script(session, """
+ return {
+ toJSON() {
+ return ["foo", "bar"];
+ }
+ };
+ """)
+ value = assert_success(response)
+ assert value == ["foo", "bar"]
+
+
+def test_object_to_json_exception(session):
+ response = execute_script(session, """
+ return {
+ toJSON() {
+ throw Error("fail");
+ }
+ };
+ """)
+ assert_error(response, "javascript error")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/promise.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/promise.py
new file mode 100644
index 0000000000..c206674bae
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/promise.py
@@ -0,0 +1,102 @@
+from tests.support.asserts import assert_error, assert_success
+from . import execute_script
+
+
+def test_promise_resolve(session):
+ response = execute_script(session, """
+ return Promise.resolve('foobar');
+ """)
+ assert_success(response, "foobar")
+
+
+def test_promise_resolve_delayed(session):
+ response = execute_script(session, """
+ return new Promise(
+ (resolve) => setTimeout(
+ () => resolve('foobar'),
+ 50
+ )
+ );
+ """)
+ assert_success(response, "foobar")
+
+
+def test_promise_all_resolve(session):
+ response = execute_script(session, """
+ return Promise.all([
+ Promise.resolve(1),
+ Promise.resolve(2)
+ ]);
+ """)
+ assert_success(response, [1, 2])
+
+
+def test_await_promise_resolve(session):
+ response = execute_script(session, """
+ let res = await Promise.resolve('foobar');
+ return res;
+ """)
+ assert_success(response, "foobar")
+
+
+def test_promise_resolve_timeout(session):
+ session.timeouts.script = .1
+ response = execute_script(session, """
+ return new Promise(
+ (resolve) => setTimeout(
+ () => resolve(),
+ 1000
+ )
+ );
+ """)
+ assert_error(response, "script timeout")
+
+
+def test_promise_reject(session):
+ response = execute_script(session, """
+ return Promise.reject(new Error('my error'));
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_promise_reject_delayed(session):
+ response = execute_script(session, """
+ return new Promise(
+ (resolve, reject) => setTimeout(
+ () => reject(new Error('my error')),
+ 50
+ )
+ );
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_promise_all_reject(session):
+ response = execute_script(session, """
+ return Promise.all([
+ Promise.resolve(1),
+ Promise.reject(new Error('error'))
+ ]);
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_await_promise_reject(session):
+ response = execute_script(session, """
+ await Promise.reject(new Error('my error'));
+ return 'foo';
+ """)
+ assert_error(response, "javascript error")
+
+
+def test_promise_reject_timeout(session):
+ session.timeouts.script = .1
+ response = execute_script(session, """
+ return new Promise(
+ (resolve, reject) => setTimeout(
+ () => reject(new Error('my error')),
+ 1000
+ )
+ );
+ """)
+ assert_error(response, "script timeout")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/properties.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/properties.py
new file mode 100644
index 0000000000..c3b01dea29
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/properties.py
@@ -0,0 +1,60 @@
+from tests.support.asserts import assert_same_element, assert_success
+from . import execute_script
+
+
+def test_content_attribute(session, inline):
+ session.url = inline("<input value=foobar>")
+ response = execute_script(session, """
+ const input = document.querySelector("input");
+ return input.value;
+ """)
+ assert_success(response, "foobar")
+
+
+def test_idl_attribute(session, inline):
+ session.url = inline("""
+ <input>
+ <script>
+ const input = document.querySelector("input");
+ input.value = "foobar";
+ </script>
+ """)
+ response = execute_script(session, """
+ const input = document.querySelector("input");
+ return input.value;
+ """)
+ assert_success(response, "foobar")
+
+
+def test_idl_attribute_element(session, inline):
+ session.url = inline("""
+ <p>foo
+ <p>bar
+
+ <script>
+ const elements = document.querySelectorAll("p");
+ let foo = elements[0];
+ let bar = elements[1];
+ foo.bar = bar;
+ </script>
+ """)
+ _foo, bar = session.find.css("p")
+ response = execute_script(session, """
+ const foo = document.querySelector("p");
+ return foo.bar;
+ """)
+ value = assert_success(response)
+ assert_same_element(session, bar, value)
+
+
+def test_script_defining_property(session, inline):
+ session.url = inline("<input>")
+ execute_script(session, """
+ const input = document.querySelector("input");
+ input.foobar = "foobar";
+ """)
+ response = execute_script(session, """
+ const input = document.querySelector("input");
+ return input.foobar;
+ """)
+ assert_success(response, "foobar")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/user_prompts.py
new file mode 100644
index 0000000000..74d4c47fc0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/user_prompts.py
@@ -0,0 +1,189 @@
+# META: timeout=long
+
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+from tests.support.sync import Poll
+from . import execute_script
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+ page_target = url("/webdriver/tests/support/html/default.html")
+
+ session.url = page_beforeunload
+
+ element = session.find.css("input", all=False)
+ element.send_keys("bar")
+
+ response = execute_script(
+ session, "window.location.href = arguments[0];", args=(page_target,))
+ assert_success(response)
+
+ wait = Poll(
+ session,
+ timeout=5,
+ message="Target page did not load")
+ wait.until(lambda s: s.url == page_target)
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = execute_script(session, "window.result = 1; return 1;")
+ assert_success(response, 1)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.execute_script("return window.result;") == 1
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = execute_script(session, "window.result = 1; return 1;")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.execute_script("return window.result;") is None
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = execute_script(session, "window.result = 1; return 1;")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.execute_script("return window.result;") is None
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/execute_script/window.py b/testing/web-platform/tests/webdriver/tests/classic/execute_script/window.py
new file mode 100644
index 0000000000..9ab45d7cb7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/execute_script/window.py
@@ -0,0 +1,87 @@
+import pytest
+
+from webdriver.client import WebFrame, WebWindow
+
+from tests.support.asserts import assert_success
+from . import execute_script
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("window.frames[0]", WebFrame),
+ ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference(session, get_test_page, expression, expected_type):
+ session.url = get_test_page()
+
+ result = execute_script(session, f"return {expression}")
+ reference = assert_success(result)
+
+ assert isinstance(reference, expected_type)
+
+ if isinstance(reference, WebWindow):
+ assert reference.id in session.handles
+ else:
+ assert reference.id not in session.handles
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("window.frames[0]", WebFrame),
+ ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference_in_array(session, get_test_page, expression, expected_type):
+ session.url = get_test_page()
+
+ result = execute_script(session, f"return [{expression}]")
+ value = assert_success(result)
+
+ assert isinstance(value[0], expected_type)
+
+ if isinstance(value[0], WebWindow):
+ assert value[0].id in session.handles
+ else:
+ assert value[0].id not in session.handles
+
+
+@pytest.mark.parametrize("expression, expected_type", [
+ ("window.frames[0]", WebFrame),
+ ("window", WebWindow),
+], ids=["frame", "window"])
+def test_web_reference_in_object(session, get_test_page, expression, expected_type):
+ session.url = get_test_page()
+
+ result = execute_script(session, f"""return {{"ref": {expression}}}""")
+ reference = assert_success(result)
+
+ assert isinstance(reference["ref"], expected_type)
+
+ if isinstance(reference["ref"], WebWindow):
+ assert reference["ref"].id in session.handles
+ else:
+ assert reference["ref"].id not in session.handles
+
+
+def test_window_open(session):
+ result = execute_script(session, "window.foo = window.open(); return window.foo;")
+ reference = assert_success(result)
+
+ assert isinstance(reference, WebWindow)
+ assert reference.id in session.handles
+
+
+def test_same_id_after_cross_origin_navigation(session, get_test_page):
+ params = {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"}
+
+ first_page = get_test_page(parameters=params, protocol="https")
+ second_page = get_test_page(parameters=params, protocol="https", domain="alt")
+
+ session.url = first_page
+
+ result = execute_script(session, "return window")
+ window_before = assert_success(result)
+
+ session.url = second_page
+
+ result = execute_script(session, "return window")
+ window_after = assert_success(result)
+
+ assert window_before == window_after
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/find_element/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element/find.py b/testing/web-platform/tests/webdriver/tests/classic/find_element/find.py
new file mode 100644
index 0000000000..50de92554b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element/find.py
@@ -0,0 +1,121 @@
+import pytest
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def find_element(session, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/element".format(**vars(session)),
+ {"using": using, "value": value})
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/element".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = find_element(session, "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = find_element(session, "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize(
+ "selector",
+ ["#same1", "#in-frame", "#in-shadow-root"],
+ ids=["not-existent", "existent-other-frame", "existent-inside-shadow-root"],
+)
+def test_no_such_element_with_unknown_selector(session, get_test_page, selector):
+ session.url = get_test_page()
+
+ response = find_element(session, "css selector", selector)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("using", ["a", True, None, 1, [], {}])
+def test_invalid_using_argument(session, using):
+ response = find_element(session, using, "value")
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, [], {}])
+def test_invalid_selector_argument(session, value):
+ response = find_element(session, "css selector", value)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//a")])
+def test_find_element(session, inline, using, value):
+ session.url = inline("<a href=# id=linkText>full link text</a>")
+
+ response = find_element(session, using, value)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>link text</a>", "link text"),
+ ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+ ("<a href=#>link<br>text</a>", "link\ntext"),
+ ("<a href=#>link&amp;text</a>", "link&text"),
+ ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+ ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_element_link_text(session, inline, document, value):
+ session.url = inline(document)
+
+ response = find_element(session, "link text", value)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>partial link text</a>", "link"),
+ ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+ ("<a href=#>partial link text</a>", "k t"),
+ ("<a href=#>partial link<br>text</a>", "k\nt"),
+ ("<a href=#>partial link&amp;text</a>", "k&t"),
+ ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+ ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_element_partial_link_text(session, inline, document, value):
+ session.url = inline(document)
+
+ response = find_element(session, "partial link text", value)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//*[name()='a']")])
+def test_xhtml_namespace(session, inline, using, value):
+ session.url = inline("""<a href="#" id="linkText">full link text</a>""",
+ doctype="xhtml")
+ expected = session.execute_script("return document.links[0]")
+
+ response = find_element(session, using, value)
+ value = assert_success(response)
+ assert_same_element(session, value, expected)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", ":root"),
+ ("tag name", "html"),
+ ("xpath", "/html")])
+def test_htmldocument(session, inline, using, value):
+ session.url = inline("")
+ response = find_element(session, using, value)
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/find_element/user_prompts.py
new file mode 100644
index 0000000000..ada8e8ebee
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element/user_prompts.py
@@ -0,0 +1,120 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import (
+ assert_error,
+ assert_same_element,
+ assert_success,
+ assert_dialog_handled,
+)
+
+
+def find_element(session, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/element".format(**vars(session)),
+ {"using": using, "value": value})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<p>bar</p>")
+ element = session.find.css("p", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, "css selector", "p")
+ value = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_same_element(session, value, element)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<p>bar</p>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<p>bar</p>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/find.py b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/find.py
new file mode 100644
index 0000000000..102704cd8e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/find.py
@@ -0,0 +1,179 @@
+import pytest
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def find_element(session, element_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/element".format(
+ session_id=session.session_id,
+ element_id=element_id),
+ {"using": using, "value": value})
+
+
+def test_null_parameter_value(session, http, inline):
+ session.url = inline("<div><a href=# id=linkText>full link text</a></div>")
+ element = session.find.css("div", all=False)
+
+ path = "/session/{session_id}/element/{element_id}/element".format(
+ session_id=session.session_id, element_id=element.id)
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = find_element(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = find_element(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = find_element(session, element.shadow_root.id, "css selector", "#in-shadow-dom")
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize(
+ "selector",
+ ["#same1", "#in-frame", "#in-shadow-dom"],
+ ids=["not-existent", "existent-other-frame", "existent-inside-shadow-root"],
+)
+def test_no_such_element_with_unknown_selector(session, get_test_page, selector):
+ session.url = get_test_page()
+
+ from_element = session.find.css(":root", all=False)
+ response = find_element(session, from_element.id, "css selector", selector)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_startnode_from_other_window_handle(session, inline):
+ session.url = inline("<div id='parent'><p/>")
+ from_element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ response = find_element(session, from_element.id, "css selector", "p")
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_startnode_from_other_frame(session, iframe, inline):
+ session.url = inline(iframe("<div id='parent'><p/>"))
+
+ session.switch_frame(0)
+ from_element = session.find.css("#parent", all=False)
+ session.switch_frame("parent")
+
+ response = find_element(session, from_element.id, "css selector", "p")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("div#with-children", as_frame=as_frame)
+
+ response = find_element(session, element.id, "css selector", "p")
+ assert_error(response, "stale element reference")
+
+
+@pytest.mark.parametrize("using", ["a", True, None, 1, [], {}])
+def test_invalid_using_argument(session, using):
+ response = find_element(session, "notReal", using, "value")
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, [], {}])
+def test_invalid_selector_argument(session, value):
+ response = find_element(session, "notReal", "css selector", value)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//a")])
+def test_find_element(session, inline, using, value):
+ session.url = inline("<div><a href=# id=linkText>full link text</a></div>")
+ element = session.find.css("div", all=False)
+ response = find_element(session, element.id, using, value)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>link text</a>", "link text"),
+ ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+ ("<a href=#>link<br>text</a>", "link\ntext"),
+ ("<a href=#>link&amp;text</a>", "link&text"),
+ ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+ ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_element_link_text(session, inline, document, value):
+ # Step 8 - 9
+ session.url = inline("<div>{0}</div>".format(document))
+ element = session.find.css("div", all=False)
+
+ response = find_element(session, element.id, "link text", value)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>partial link text</a>", "link"),
+ ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+ ("<a href=#>partial link text</a>", "k t"),
+ ("<a href=#>partial link<br>text</a>", "k\nt"),
+ ("<a href=#>partial link&amp;text</a>", "k&t"),
+ ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+ ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_element_partial_link_text(session, inline, document, value):
+ session.url = inline("<div>{0}</div>".format(document))
+ element = session.find.css("div", all=False)
+
+ response = find_element(session, element.id, "partial link text", value)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//*[name()='a']")])
+def test_xhtml_namespace(session, inline, using, value):
+ session.url = inline("""<p><a href="#" id="linkText">full link text</a></p>""",
+ doctype="xhtml")
+ from_element = session.execute_script("""return document.querySelector("p")""")
+ expected = session.execute_script("return document.links[0]")
+
+ response = find_element(session, from_element.id, using, value)
+ value = assert_success(response)
+ assert_same_element(session, value, expected)
+
+
+def test_parent_htmldocument(session, inline):
+ session.url = inline("")
+ from_element = session.execute_script("""return document.querySelector("body")""")
+ expected = session.execute_script("return document.documentElement")
+
+ response = find_element(session, from_element.id, "xpath", "..")
+ value = assert_success(response)
+ assert_same_element(session, value, expected)
+
+
+def test_parent_of_document_node_errors(session, inline):
+ session.url = inline("")
+ from_element = session.execute_script("return document.documentElement")
+
+ response = find_element(session, from_element.id, "xpath", "..")
+ assert_error(response, "invalid selector")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/user_prompts.py
new file mode 100644
index 0000000000..0537a78618
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_element/user_prompts.py
@@ -0,0 +1,125 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import (
+ assert_error,
+ assert_same_element,
+ assert_success,
+ assert_dialog_handled,
+)
+
+
+def find_element(session, element_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/element".format(
+ session_id=session.session_id,
+ element_id=element_id),
+ {"using": using, "value": value})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<div><p>bar</p><div>")
+ outer_element = session.find.css("div", all=False)
+ inner_element = session.find.css("p", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, outer_element.id, "css selector", "p")
+ value = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_same_element(session, value, inner_element)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<div><p>bar</p><div>")
+ outer_element = session.find.css("div", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, outer_element.id, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<div><p>bar</p><div>")
+ outer_element = session.find.css("div", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, outer_element.id, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/find.py b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/find.py
new file mode 100644
index 0000000000..1b4e739419
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/find.py
@@ -0,0 +1,247 @@
+import pytest
+from webdriver.client import WebElement, ShadowRoot
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def find_element(session, shadow_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/shadow/{shadow_id}/element".format(
+ session_id=session.session_id,
+ shadow_id=shadow_id),
+ {"using": using, "value": value})
+
+
+def test_null_parameter_value(session, http, get_test_page):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ path = "/session/{session_id}/shadow/{shadow_id}/element".format(
+ session_id=session.session_id, shadow_id=shadow_root.id)
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = find_element(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = find_element(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_shadow_root_with_element(session, get_test_page):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+
+ result = find_element(session, host.id, "css selector", "input")
+ assert_error(result, "no such shadow root")
+
+
+def test_no_such_shadow_root_with_unknown_shadow_root(session):
+ shadow_root = ShadowRoot(session, "foo")
+
+ result = find_element(session, shadow_root.id, "css selector", "input")
+ assert_error(result, "no such shadow root")
+
+
+def test_no_such_shadow_root_with_shadow_root_from_other_window_handle(
+ session, get_test_page
+):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ response = find_element(session, shadow_root.id, "css selector", "div")
+ assert_error(response, "no such shadow root")
+
+
+def test_no_such_shadow_root_with_shadow_root_from_other_frame(
+ session, get_test_page
+):
+ session.url = get_test_page(as_frame=True)
+ session.switch_frame(0)
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ session.switch_frame("parent")
+
+ response = find_element(session, shadow_root.id, "css selector", "div")
+ assert_error(response, "no such shadow root")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_detached_shadow_root(session, get_test_page, as_frame):
+ session.url = get_test_page(as_frame=as_frame)
+
+ if as_frame:
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ session.execute_script("arguments[0].remove();", args=[host])
+
+ response = find_element(session, shadow_root.id, "css selector", "input")
+ assert_error(response, "detached shadow root")
+
+
+@pytest.mark.parametrize(
+ "selector",
+ ["#same1", "#in-frame", "#with-children"],
+ ids=["not-existent", "existent-other-frame", "existent-outside-shadow-root"],
+)
+def test_no_such_element_with_unknown_selector(session, get_test_page, selector):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ response = find_element(session, shadow_root.id, "css selector", selector)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("shadow_root_id", [True, None, 1, [], {}])
+def test_invalid_shadow_root_id_argument(session, get_test_page, shadow_root_id):
+ session.url = get_test_page()
+
+ response = find_element(session, shadow_root_id, "css selector", "input")
+ assert_error(response, "no such shadow root")
+
+
+@pytest.mark.parametrize("using", ["a", True, None, 1, [], {}])
+def test_invalid_using_argument(session, get_test_page, using):
+ session.url = get_test_page()
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ response = find_element(session, shadow_root.id, using, "input")
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, [], {}])
+def test_invalid_selector_argument(session, get_test_page, value):
+ session.url = get_test_page()
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ response = find_element(session, shadow_root.id, "css selector", value)
+ assert_error(response, "invalid argument")
+
+
+def test_found_element_equivalence(session, get_test_page):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ expected = session.execute_script("""
+ return arguments[0].shadowRoot.querySelector('input')
+ """, args=(host,))
+
+ response = find_element(session, shadow_root.id, "css selector", "input")
+ value = assert_success(response)
+ assert_same_element(session, value, expected)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//a")])
+@pytest.mark.parametrize("mode", ["open", "closed"])
+def test_find_element(session, get_test_page, using, value, mode):
+ expected_text = "full link text"
+ session.url = get_test_page(
+ shadow_doc=f"<div><a href=# id=linkText>{expected_text}</a></div>",
+ shadow_root_mode=mode,
+ )
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ result = find_element(session, shadow_root.id, using, value)
+ value = assert_success(result)
+
+ element = WebElement.from_json(value, session)
+ assert element.text == expected_text
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>link text</a>", "link text"),
+ ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+ ("<a href=#>link<br>text</a>", "link\ntext"),
+ ("<a href=#>link&amp;text</a>", "link&text"),
+ ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+ ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_element_link_text(session, get_test_page, document, value):
+ session.url = get_test_page(shadow_doc=f"<div>{document}</div>")
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ expected = session.execute_script("""
+ return arguments[0].shadowRoot.querySelectorAll('a')[0]
+ """, args=(host,))
+
+ response = find_element(session, shadow_root.id, "link text", value)
+ value = assert_success(response)
+ assert_same_element(session, value, expected)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>partial link text</a>", "link"),
+ ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+ ("<a href=#>partial link text</a>", "k t"),
+ ("<a href=#>partial link<br>text</a>", "k\nt"),
+ ("<a href=#>partial link&amp;text</a>", "k&t"),
+ ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+ ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_element_partial_link_text(session, get_test_page, document, value):
+ session.url = get_test_page(shadow_doc=f"<div>{document}</div>")
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ expected = session.execute_script("""
+ return arguments[0].shadowRoot.querySelectorAll('a')[0]
+ """, args=(host,))
+
+ response = find_element(session, shadow_root.id, "partial link text", value)
+ value = assert_success(response)
+ assert_same_element(session, value, expected)
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+def test_find_element_in_nested_shadow_root(session, get_test_page, mode):
+ expected_text = "full link text"
+ session.url = get_test_page(
+ shadow_doc=f"<div><a href=# id=linkText>{expected_text}</a></div>",
+ shadow_root_mode=mode,
+ nested_shadow_dom=True,
+ )
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ inner_custom_element = shadow_root.find_element(
+ "css selector", "inner-custom-element"
+ )
+ nested_shadow_root = inner_custom_element.shadow_root
+
+ result = find_element(session, nested_shadow_root.id, "css selector", "#linkText")
+ value = assert_success(result)
+
+ element = WebElement.from_json(value, session)
+ assert element.text == expected_text
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/user_prompts.py
new file mode 100644
index 0000000000..3e3381e785
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_element_from_shadow_root/user_prompts.py
@@ -0,0 +1,134 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import (
+ assert_error,
+ assert_same_element,
+ assert_success,
+ assert_dialog_handled,
+)
+
+
+def find_element(session, shadow_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/shadow/{shadow_id}/element".format(
+ session_id=session.session_id,
+ shadow_id=shadow_id),
+ {"using": using, "value": value})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ inner_element = session.execute_script("""
+ return arguments[0].shadowRoot.querySelector('input')
+ """, args=(host,))
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, shadow_root.id, "css selector", "input")
+ value = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_same_element(session, value, inner_element)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, shadow_root.id, "css selector", "input")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_element(session, shadow_root.id, "css selector", "input")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements/find.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements/find.py
new file mode 100644
index 0000000000..0d9ce21186
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements/find.py
@@ -0,0 +1,141 @@
+import pytest
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def find_elements(session, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/elements".format(**vars(session)),
+ {"using": using, "value": value})
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/elements".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = find_elements(session, "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = find_elements(session, "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize(
+ "selector",
+ ["#same1", "#in-frame", "#in-shadow-dom"],
+ ids=["not-existent", "existent-other-frame", "existent-inside-shadow-root"],
+)
+def test_no_elements_with_unknown_selector(session, get_test_page,selector):
+ session.url = get_test_page()
+
+ response = find_elements(session, "css selector", selector)
+ elements = assert_success(response)
+ assert elements == []
+
+
+@pytest.mark.parametrize("using", ["a", True, None, 1, [], {}])
+def test_invalid_using_argument(session, using):
+ response = find_elements(session, using, "value")
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, [], {}])
+def test_invalid_selector_argument(session, value):
+ response = find_elements(session, "css selector", value)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//a")])
+def test_find_elements(session, inline, using, value):
+ session.url = inline("<a href=# id=linkText>full link text</a>")
+
+ response = find_elements(session, using, value)
+ assert_success(response)
+ assert len(response.body["value"]) == 1
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>link text</a>", "link text"),
+ ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+ ("<a href=#>link<br>text</a>", "link\ntext"),
+ ("<a href=#>link&amp;text</a>", "link&text"),
+ ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+ ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_elements_link_text(session, inline, document, value):
+ session.url = inline("<a href=#>not wanted</a><br/>{0}".format(document))
+ expected = session.execute_script("return document.links[1];")
+
+ response = find_elements(session, "link text", value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ found_element = value[0]
+ assert_same_element(session, found_element, expected)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>partial link text</a>", "link"),
+ ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+ ("<a href=#>partial link text</a>", "k t"),
+ ("<a href=#>partial link<br>text</a>", "k\nt"),
+ ("<a href=#>partial link&amp;text</a>", "k&t"),
+ ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+ ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_elements_partial_link_text(session, inline, document, value):
+ session.url = inline("<a href=#>not wanted</a><br/>{0}".format(document))
+ expected = session.execute_script("return document.links[1];")
+
+ response = find_elements(session, "partial link text", value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ found_element = value[0]
+ assert_same_element(session, found_element, expected)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//*[name()='a']")])
+def test_xhtml_namespace(session, inline, using, value):
+ session.url = inline("""<a href="#" id="linkText">full link text</a>""",
+ doctype="xhtml")
+ expected = session.execute_script("return document.links[0];")
+
+ response = find_elements(session, using, value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ found_element = value[0]
+ assert_same_element(session, found_element, expected)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", ":root"),
+ ("tag name", "html"),
+ ("xpath", "/html")])
+def test_htmldocument(session, inline, using, value):
+ session.url = inline("")
+ response = find_elements(session, using, value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements/user_prompts.py
new file mode 100644
index 0000000000..f9a45e5275
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements/user_prompts.py
@@ -0,0 +1,122 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import (
+ assert_error,
+ assert_same_element,
+ assert_success,
+ assert_dialog_handled,
+)
+
+
+def find_elements(session, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/elements".format(**vars(session)),
+ {"using": using, "value": value})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<p>bar</p>")
+ element = session.find.css("p", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, "css selector", "p")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_same_element(session, value[0], element)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<p>bar</p>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<p>bar</p>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/find.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/find.py
new file mode 100644
index 0000000000..fc898bc95a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/find.py
@@ -0,0 +1,199 @@
+import pytest
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def find_elements(session, element_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/elements".format(
+ session_id=session.session_id,
+ element_id=element_id),
+ {"using": using, "value": value})
+
+
+def test_null_parameter_value(session, http, inline):
+ session.url = inline("<div><a href=# id=linkText>full link text</a></div>")
+ element = session.find.css("div", all=False)
+
+ path = "/session/{session_id}/element/{element_id}/elements".format(
+ session_id=session.session_id, element_id=element.id)
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = find_elements(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = find_elements(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = find_elements(session, element.shadow_root.id, "css selector", "#in-shadow-dom")
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize(
+ "selector",
+ ["#same1", "#in-frame", "#in-shadow-dom"],
+ ids=["not-existent", "existent-other-frame", "existent-inside-shadow-root"],
+)
+def test_no_elements_with_unknown_selector(session, get_test_page,selector):
+ session.url = get_test_page()
+
+ element = session.find.css(":root", all=False)
+ response = find_elements(session, element.id, "css selector", selector)
+ elements = assert_success(response)
+ assert elements == []
+
+
+def test_no_such_element_with_startnode_from_other_window_handle(session, inline):
+ session.url = inline("<div id='parent'><p/>")
+ from_element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ response = find_elements(session, from_element.id, "css selector", "p")
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_startnode_from_other_frame(session, iframe, inline):
+ session.url = inline(iframe("<div id='parent'><p/>"))
+
+ session.switch_frame(0)
+ from_element = session.find.css("#parent", all=False)
+ session.switch_frame("parent")
+
+ response = find_elements(session, from_element.id, "css selector", "p")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("div#with-children", as_frame=as_frame)
+
+ response = find_elements(session, element.id, "css selector", "p")
+ assert_error(response, "stale element reference")
+
+
+@pytest.mark.parametrize("using", [("a"), (True), (None), (1), ([]), ({})])
+def test_invalid_using_argument(session, using):
+ response = find_elements(session, "notReal", using, "value")
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, [], {}])
+def test_invalid_selector_argument(session, value):
+ response = find_elements(session, "notReal", "css selector", value)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//a")])
+def test_find_elements(session, inline, using, value):
+ session.url = inline("<div><a href=# id=linkText>full link text</a></div>")
+ element = session.find.css("div", all=False)
+ response = find_elements(session, element.id, using, value)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>link text</a>", "link text"),
+ ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+ ("<a href=#>link<br>text</a>", "link\ntext"),
+ ("<a href=#>link&amp;text</a>", "link&text"),
+ ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+ ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_elements_link_text(session, inline, document, value):
+ session.url = inline("<div><a href=#>not wanted</a><br/>{0}</div>".format(document))
+ element = session.find.css("div", all=False)
+ expected = session.execute_script("return document.links[1];")
+
+ response = find_elements(session, element.id, "link text", value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ found_element = value[0]
+ assert_same_element(session, found_element, expected)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>partial link text</a>", "link"),
+ ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+ ("<a href=#>partial link text</a>", "k t"),
+ ("<a href=#>partial link<br>text</a>", "k\nt"),
+ ("<a href=#>partial link&amp;text</a>", "k&t"),
+ ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+ ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_elements_partial_link_text(session, inline, document, value):
+ session.url = inline("<div><a href=#>not wanted</a><br/>{0}</div>".format(document))
+ element = session.find.css("div", all=False)
+ expected = session.execute_script("return document.links[1];")
+
+ response = find_elements(session, element.id, "partial link text", value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ found_element = value[0]
+ assert_same_element(session, found_element, expected)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//*[name()='a']")])
+def test_xhtml_namespace(session, inline, using, value):
+ session.url = inline("""<p><a href="#" id="linkText">full link text</a></p>""",
+ doctype="xhtml")
+ from_element = session.execute_script("""return document.querySelector("p")""")
+ expected = session.execute_script("return document.links[0]")
+
+ response = find_elements(session, from_element.id, using, value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ found_element = value[0]
+ assert_same_element(session, found_element, expected)
+
+
+def test_parent_htmldocument(session, inline):
+ session.url = inline("")
+ from_element = session.execute_script("""return document.querySelector("body")""")
+ expected = session.execute_script("return document.documentElement")
+
+ response = find_elements(session, from_element.id, "xpath", "..")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ found_element = value[0]
+ assert_same_element(session, found_element, expected)
+
+
+def test_parent_of_document_node_errors(session, inline):
+ session.url = inline("")
+ from_element = session.execute_script("return document.documentElement")
+
+ response = find_elements(session, from_element.id, "xpath", "..")
+ assert_error(response, "invalid selector")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/user_prompts.py
new file mode 100644
index 0000000000..467bec09a1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_element/user_prompts.py
@@ -0,0 +1,127 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import (
+ assert_error,
+ assert_same_element,
+ assert_success,
+ assert_dialog_handled,
+)
+
+
+def find_elements(session, element_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/element/{element_id}/elements".format(
+ session_id=session.session_id,
+ element_id=element_id),
+ {"using": using, "value": value})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<div><p>bar</p><div>")
+ outer_element = session.find.css("div", all=False)
+ inner_element = session.find.css("p", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, outer_element.id, "css selector", "p")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_same_element(session, value[0], inner_element)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<div><p>bar</p><div>")
+ outer_element = session.find.css("div", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, outer_element.id, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<div><p>bar</p><div>")
+ outer_element = session.find.css("div", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, outer_element.id, "css selector", "p")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/find.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/find.py
new file mode 100644
index 0000000000..1e977f2f21
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/find.py
@@ -0,0 +1,260 @@
+import pytest
+from webdriver.client import WebElement, ShadowRoot
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def find_elements(session, shadow_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/shadow/{shadow_id}/elements".format(
+ session_id=session.session_id,
+ shadow_id=shadow_id),
+ {"using": using, "value": value})
+
+
+def test_null_parameter_value(session, http, get_test_page):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ path = "/session/{session_id}/shadow/{shadow_id}/elements".format(
+ session_id=session.session_id, shadow_id=shadow_root.id)
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = find_elements(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = find_elements(session, "notReal", "css selector", "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_shadow_root_with_element(session, get_test_page):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+
+ result = find_elements(session, host.id, "css selector", "input")
+ assert_error(result, "no such shadow root")
+
+
+def test_no_such_shadow_root_with_unknown_shadow_root(session):
+ shadow_root = ShadowRoot(session, "foo")
+
+ result = find_elements(session, shadow_root.id, "css selector", "input")
+ assert_error(result, "no such shadow root")
+
+
+def test_no_such_shadow_root_with_shadow_root_from_other_window_handle(
+ session, get_test_page
+):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ new_handle = session.new_window()
+ session.window_handle = new_handle
+
+ response = find_elements(session, shadow_root.id, "css selector", "div")
+ assert_error(response, "no such shadow root")
+
+
+def test_no_such_shadow_root_with_shadow_root_from_other_frame(
+ session, get_test_page
+):
+ session.url = get_test_page(as_frame=True)
+ session.switch_frame(0)
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ session.switch_frame("parent")
+
+ response = find_elements(session, shadow_root.id, "css selector", "div")
+ assert_error(response, "no such shadow root")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_detached_shadow_root(session, get_test_page, as_frame):
+ session.url = get_test_page(as_frame=as_frame)
+
+ if as_frame:
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ session.execute_script("arguments[0].remove();", args=[host])
+
+ response = find_elements(session, shadow_root.id, "css selector", "input")
+ assert_error(response, "detached shadow root")
+
+
+@pytest.mark.parametrize(
+ "selector",
+ ["#same1", "#in-frame", "#with-children"],
+ ids=["not-existent", "existent-other-frame", "existent-outside-shadow-root"],
+)
+def test_no_elements_with_unknown_selector(session, get_test_page,selector):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ response = find_elements(session, shadow_root.id, "css selector", selector)
+ elements = assert_success(response)
+ assert elements == []
+
+
+@pytest.mark.parametrize("shadow_root_id", [True, None, 1, [], {}])
+def test_invalid_shadow_root_id_argument(session, get_test_page, shadow_root_id):
+ session.url = get_test_page()
+
+ response = find_elements(session, shadow_root_id, ("css selector"), "input")
+ assert_error(response, "no such shadow root")
+
+
+@pytest.mark.parametrize("using", [("a"), (True), (None), (1), ([]), ({})])
+def test_invalid_using_argument(session, get_test_page, using):
+ session.url = get_test_page()
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ response = find_elements(session, shadow_root.id, using, "input")
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, [], {}])
+def test_invalid_selector_argument(session, get_test_page, value):
+ session.url = get_test_page()
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ response = find_elements(session, shadow_root.id, "css selector", value)
+ assert_error(response, "invalid argument")
+
+
+def test_find_elements_equivalence(session, get_test_page):
+ session.url = get_test_page(
+ shadow_doc="<div><input id='check' type='checkbox'/><input id='text'/></div>")
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ session.execute_script("""
+ return arguments[0].shadowRoot.querySelector('input')
+ """, args=(host,))
+
+ response = find_elements(session, shadow_root.id, "css selector", "input")
+ assert_success(response)
+
+
+@pytest.mark.parametrize("using,value",
+ [("css selector", "#linkText"),
+ ("link text", "full link text"),
+ ("partial link text", "link text"),
+ ("tag name", "a"),
+ ("xpath", "//a")])
+@pytest.mark.parametrize("mode", ["open", "closed"])
+def test_find_elements(session, get_test_page, using, value, mode):
+ expected_text = "full link text"
+ session.url = get_test_page(
+ shadow_doc=f"<div><a href=# id=linkText>{expected_text}</a></div>",
+ shadow_root_mode=mode,
+ )
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ result = find_elements(session, shadow_root.id, using, value)
+ value = assert_success(result)
+
+ assert len(value) == 1
+
+ element = WebElement.from_json(value[0], session)
+ assert element.text == expected_text
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>link text</a>", "link text"),
+ ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+ ("<a href=#>link<br>text</a>", "link\ntext"),
+ ("<a href=#>link&amp;text</a>", "link&text"),
+ ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+ ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_elements_link_text(session, get_test_page, document, value):
+ session.url = get_test_page(shadow_doc=f"<div><a href=#>not wanted</a><br/>{document}</div>")
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ expected = session.execute_script("""
+ return arguments[0].shadowRoot.querySelectorAll('a')[1]
+ """, args=(host,))
+
+ response = find_elements(session, shadow_root.id, "link text", value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ assert_same_element(session, value[0], expected)
+
+
+@pytest.mark.parametrize("document,value", [
+ ("<a href=#>partial link text</a>", "link"),
+ ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+ ("<a href=#>partial link text</a>", "k t"),
+ ("<a href=#>partial link<br>text</a>", "k\nt"),
+ ("<a href=#>partial link&amp;text</a>", "k&t"),
+ ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+ ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_elements_partial_link_text(session, get_test_page, document, value):
+ session.url = get_test_page(shadow_doc=f"<div><a href=#>not wanted</a><br/>{document}</div>")
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ expected = session.execute_script("""
+ return arguments[0].shadowRoot.querySelectorAll('a')[1]
+ """, args=(host,))
+
+ response = find_elements(session, shadow_root.id, "partial link text", value)
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ assert_same_element(session, value[0], expected)
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+def test_find_elements_in_nested_shadow_root(
+ session, get_test_page, mode
+):
+ expected_text = "full link text"
+ session.url = get_test_page(
+ shadow_doc=f"<div><a href=# id=linkText>{expected_text}</a></div>",
+ shadow_root_mode=mode,
+ nested_shadow_dom=True,
+ )
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ inner_custom_element = shadow_root.find_element(
+ "css selector", "inner-custom-element"
+ )
+ nested_shadow_root = inner_custom_element.shadow_root
+
+ result = find_elements(session, nested_shadow_root.id, "css selector", "#linkText")
+ value = assert_success(result)
+
+ assert len(value) == 1
+
+ element = WebElement.from_json(value[0], session)
+ assert element.text == expected_text
diff --git a/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/user_prompts.py
new file mode 100644
index 0000000000..45986ad6da
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/find_elements_from_shadow_root/user_prompts.py
@@ -0,0 +1,135 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import (
+ assert_error,
+ assert_same_element,
+ assert_success,
+ assert_dialog_handled,
+)
+
+
+def find_elements(session, shadow_id, using, value):
+ return session.transport.send(
+ "POST", "session/{session_id}/shadow/{shadow_id}/elements".format(
+ session_id=session.session_id,
+ shadow_id=shadow_id),
+ {"using": using, "value": value})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+ inner_element = session.execute_script("""
+ return arguments[0].shadowRoot.querySelector("input")
+ """, args=(host,))
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, shadow_root.id, "css selector", "input")
+ value = assert_success(response)
+ assert isinstance(value, list)
+ assert len(value) == 1
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_same_element(session, value[0], inner_element)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, shadow_root.id, "css selector", "input")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = get_test_page()
+
+ host = session.find.css("custom-element", all=False)
+ shadow_root = host.shadow_root
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = find_elements(session, shadow_root.id, "css selector", "input")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/forward/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/forward/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/forward/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/forward/conftest.py b/testing/web-platform/tests/webdriver/tests/classic/forward/conftest.py
new file mode 100644
index 0000000000..bd5db0cfeb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/forward/conftest.py
@@ -0,0 +1,19 @@
+import pytest
+
+from webdriver.error import NoSuchWindowException
+
+
+@pytest.fixture(name="session")
+def fixture_session(capabilities, session):
+ """Prevent re-using existent history by running the test in a new window."""
+ original_handle = session.window_handle
+ session.window_handle = session.new_window()
+
+ yield session
+
+ try:
+ session.window.close()
+ except NoSuchWindowException:
+ pass
+
+ session.window_handle = original_handle
diff --git a/testing/web-platform/tests/webdriver/tests/classic/forward/forward.py b/testing/web-platform/tests/webdriver/tests/classic/forward/forward.py
new file mode 100644
index 0000000000..8bc75a07c0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/forward/forward.py
@@ -0,0 +1,176 @@
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def forward(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/forward".format(**vars(session)))
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<div>")
+ session.url = inline("<p>")
+ session.back()
+
+ response = forward(session)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = forward(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = forward(session)
+ assert_success(response)
+
+
+def test_basic(session, inline):
+ url = inline("<div id=foo>")
+
+ session.url = inline("<div id=bar>")
+ session.url = url
+ session.back()
+
+ element = session.find.css("#bar", all=False)
+
+ response = forward(session)
+ assert_success(response)
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.property("id")
+
+ assert session.url == url
+ assert session.find.css("#foo", all=False)
+
+
+def test_no_browsing_history(session, inline):
+ url = inline("<div id=foo>")
+
+ session.url = url
+ element = session.find.css("#foo", all=False)
+
+ response = forward(session)
+ assert_success(response)
+
+ assert session.url == url
+ assert element.property("id") == "foo"
+
+
+@pytest.mark.parametrize("protocol,parameters", [
+ ("http", ""),
+ ("https", ""),
+ ("https", {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"})
+], ids=["http", "https", "https coop"])
+def test_seen_nodes(session, get_test_page, protocol, parameters):
+ first_page = get_test_page(parameters=parameters, protocol=protocol)
+ second_page = get_test_page(parameters=parameters, protocol=protocol, domain="alt")
+
+ session.url = first_page
+ session.url = second_page
+ session.back()
+
+ element = session.find.css("#custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ response = forward(session)
+ assert_success(response)
+
+ assert session.url == second_page
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.name
+ with pytest.raises(error.DetachedShadowRootException):
+ shadow_root.find_element("css selector", "in-shadow-dom")
+
+ session.find.css("#custom-element", all=False)
+
+
+def test_data_urls(session, inline):
+ test_pages = [
+ inline("<p id=1>"),
+ inline("<p id=2>"),
+ ]
+
+ for page in test_pages:
+ session.url = page
+
+ session.back()
+ assert session.url == test_pages[0]
+
+ response = forward(session)
+ assert_success(response)
+ assert session.url == test_pages[1]
+
+
+def test_fragments(session, url):
+ test_pages = [
+ url("/common/blank.html"),
+ url("/common/blank.html#1234"),
+ url("/common/blank.html#5678"),
+ ]
+
+ for page in test_pages:
+ session.url = page
+
+ session.back()
+ assert session.url == test_pages[1]
+
+ session.back()
+ assert session.url == test_pages[0]
+
+ response = forward(session)
+ assert_success(response)
+ assert session.url == test_pages[1]
+
+ response = forward(session)
+ assert_success(response)
+ assert session.url == test_pages[2]
+
+
+def test_history_pushstate(session, inline):
+ pushstate_page = inline("""
+ <script>
+ function pushState() {
+ history.pushState({foo: "bar"}, "", "#pushstate");
+ }
+ </script>
+ <a onclick="javascript:pushState();">click</a>
+ """)
+
+ session.url = pushstate_page
+
+ session.find.css("a", all=False).click()
+ assert session.url == "{}#pushstate".format(pushstate_page)
+ assert session.execute_script("return history.state;") == {"foo": "bar"}
+
+ session.back()
+ assert session.url == pushstate_page
+ assert session.execute_script("return history.state;") is None
+
+ response = forward(session)
+ assert_success(response)
+
+ assert session.url == "{}#pushstate".format(pushstate_page)
+ assert session.execute_script("return history.state;") == {"foo": "bar"}
+
+
+def test_removed_iframe(session, url, inline):
+ page = inline("<p>foo")
+
+ session.url = url("/webdriver/tests/support/html/frames_no_bfcache.html")
+ session.url = page
+
+ session.back()
+
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ response = forward(session)
+ assert_success(response)
+
+ assert session.url == page
diff --git a/testing/web-platform/tests/webdriver/tests/classic/forward/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/forward/user_prompts.py
new file mode 100644
index 0000000000..dfcba6469d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/forward/user_prompts.py
@@ -0,0 +1,195 @@
+# META: timeout=long
+
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def forward(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/forward".format(**vars(session)))
+
+
+@pytest.fixture
+def pages(session, inline):
+ pages = [
+ inline("<p id=1>"),
+ inline("<p id=2>"),
+ ]
+
+ for page in pages:
+ session.url = page
+
+ session.back()
+
+ return pages
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+ page_target = url("/webdriver/tests/support/html/default.html")
+
+ session.url = page_beforeunload
+ session.url = page_target
+ session.back()
+
+ element = session.find.css("input", all=False)
+ element.send_keys("bar")
+
+ response = forward(session)
+ assert_success(response)
+
+ assert session.url == page_target
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, pages):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = forward(session)
+ assert_success(response)
+
+ # retval not testable for confirm and prompt because window is gone
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=None)
+
+ assert session.url == pages[1]
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, pages):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = forward(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.url == pages[0]
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, pages):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = forward(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.url == pages[0]
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/fullscreen.py b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/fullscreen.py
new file mode 100644
index 0000000000..ce9e033d64
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/fullscreen.py
@@ -0,0 +1,88 @@
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import (
+ document_hidden,
+ is_fullscreen,
+ is_maximized,
+)
+
+
+def fullscreen(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/fullscreen".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = fullscreen(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = fullscreen(session)
+ assert_success(response)
+
+
+def test_response_payload(session, screen_size):
+ assert not is_fullscreen(session)
+
+ response = fullscreen(session)
+ value = assert_success(response)
+
+ assert is_fullscreen(session)
+
+ assert isinstance(value, dict)
+ assert isinstance(value.get("x"), int)
+ assert isinstance(value.get("y"), int)
+ assert isinstance(value.get("width"), int)
+ assert isinstance(value.get("height"), int)
+
+
+def test_fullscreen_from_normal_window(session, screen_size):
+ assert not is_fullscreen(session)
+
+ response = fullscreen(session)
+ assert_success(response, session.window.rect)
+
+ assert is_fullscreen(session)
+ assert session.window.size == screen_size
+
+
+def test_fullscreen_from_maximized_window(session, screen_size):
+ assert not is_fullscreen(session)
+
+ session.window.maximize()
+ assert is_maximized(session)
+
+ response = fullscreen(session)
+ assert_success(response, session.window.rect)
+ assert not is_maximized(session)
+
+ assert session.window.size == screen_size
+
+
+def test_fullscreen_from_minimized_window(session, screen_size):
+ assert not document_hidden(session)
+
+ session.window.minimize()
+ assert document_hidden(session)
+ assert not is_fullscreen(session)
+
+ response = fullscreen(session)
+ assert_success(response, session.window.rect)
+ assert not document_hidden(session)
+ assert is_fullscreen(session)
+
+ assert session.window.size == screen_size
+
+
+def test_fullscreen_twice_is_idempotent(session, screen_size):
+ assert not is_fullscreen(session)
+
+ first_response = fullscreen(session)
+ assert_success(first_response, session.window.rect)
+ assert is_fullscreen(session)
+ assert session.window.size == screen_size
+
+ second_response = fullscreen(session)
+ assert_success(second_response, session.window.rect)
+ assert is_fullscreen(session)
+ assert session.window.size == screen_size
diff --git a/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/stress.py b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/stress.py
new file mode 100644
index 0000000000..cbc5e28c90
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/stress.py
@@ -0,0 +1,22 @@
+# META: timeout=long
+
+# Longer timeout required due to a bug in Chrome:
+# https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+
+import pytest
+
+from tests.support.asserts import assert_success
+from tests.support.helpers import is_fullscreen
+
+
+def fullscreen_window(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/fullscreen".format(**vars(session)))
+
+
+@pytest.mark.parametrize("i", range(5))
+def test_stress(session, i):
+ assert not is_fullscreen(session)
+ response = fullscreen_window(session)
+ assert_success(response)
+ assert is_fullscreen(session)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/user_prompts.py
new file mode 100644
index 0000000000..106bc457f0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/fullscreen_window/user_prompts.py
@@ -0,0 +1,116 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+from tests.support.helpers import is_fullscreen
+
+
+def fullscreen(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/fullscreen".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ assert not is_fullscreen(session)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = fullscreen(session)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+ assert is_fullscreen(session)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ assert not is_fullscreen(session)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = fullscreen(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+ assert not is_fullscreen(session)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ assert not is_fullscreen(session)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = fullscreen(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert not is_fullscreen(session)
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_active_element/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_active_element/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_active_element/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_active_element/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_active_element/get.py
new file mode 100644
index 0000000000..1d2960c88c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_active_element/get.py
@@ -0,0 +1,154 @@
+from tests.support.asserts import assert_error, assert_is_active_element, assert_success
+
+
+def read_global(session, name):
+ return session.execute_script("return %s;" % name)
+
+
+def get_active_element(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/active".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_active_element(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_active_element(session)
+ assert_error(response, "no such window")
+
+
+def test_no_such_element(session, inline):
+ session.url = inline("<body></body>")
+ session.execute_script("""
+ if (document.body.remove) {
+ document.body.remove();
+ } else {
+ document.body.removeNode(true);
+ }""")
+
+ response = get_active_element(session)
+ assert_error(response, "no such element")
+
+
+def test_success_document(session, inline):
+ session.url = inline("""
+ <body>
+ <h1>Heading</h1>
+ <input />
+ <input />
+ <input style="opacity: 0" />
+ <p>Another element</p>
+ </body>""")
+
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+
+def test_success_input(session, inline):
+ session.url = inline("""
+ <body>
+ <h1>Heading</h1>
+ <input autofocus />
+ <input style="opacity: 0" />
+ <p>Another element</p>
+ </body>""")
+
+ # Per spec, autofocus candidates will be
+ # flushed by next paint, so we use rAF here to
+ # ensure the candidates are flushed.
+ session.execute_async_script(
+ """
+ const resolve = arguments[0];
+ window.requestAnimationFrame(function() {
+ window.requestAnimationFrame(resolve);
+ });
+ """
+ )
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+
+def test_success_input_non_interactable(session, inline):
+ session.url = inline("""
+ <body>
+ <h1>Heading</h1>
+ <input />
+ <input style="opacity: 0" autofocus />
+ <p>Another element</p>
+ </body>""")
+
+ # Per spec, autofocus candidates will be
+ # flushed by next paint, so we use rAF here to
+ # ensure the candidates are flushed.
+ session.execute_async_script(
+ """
+ const resolve = arguments[0];
+ window.requestAnimationFrame(function() {
+ window.requestAnimationFrame(resolve);
+ });
+ """
+ )
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+
+def test_success_explicit_focus(session, inline):
+ session.url = inline("""
+ <body>
+ <h1>Heading</h1>
+ <input />
+ <iframe></iframe>
+ </body>""")
+
+ session.execute_script("document.body.getElementsByTagName('h1')[0].focus()")
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+ session.execute_script("document.body.getElementsByTagName('input')[0].focus()")
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+ session.execute_script("document.body.getElementsByTagName('iframe')[0].focus()")
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+ session.execute_script("document.body.getElementsByTagName('iframe')[0].focus();")
+ session.execute_script("""
+ var iframe = document.body.getElementsByTagName('iframe')[0];
+ if (iframe.remove) {
+ iframe.remove();
+ } else {
+ iframe.removeNode(true);
+ }""")
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+ session.execute_script("document.body.appendChild(document.createElement('textarea'))")
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
+
+
+def test_success_iframe_content(session, inline):
+ session.url = inline("<body></body>")
+ session.execute_script("""
+ let iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ let input = iframe.contentDocument.createElement('input');
+ iframe.contentDocument.body.appendChild(input);
+ input.focus();
+ """)
+
+ response = get_active_element(session)
+ element = assert_success(response)
+ assert_is_active_element(session, element)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_active_element/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_active_element/user_prompts.py
new file mode 100644
index 0000000000..1ff77697b7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_active_element/user_prompts.py
@@ -0,0 +1,118 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import (
+ assert_dialog_handled,
+ assert_error,
+ assert_is_active_element,
+ assert_success
+)
+
+
+def get_active_element(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/active".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_active_element(session)
+ element = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_is_active_element(session, element)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input type=text>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_active_element(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input type=text>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_active_element(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_alert_text/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_alert_text/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_alert_text/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_alert_text/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_alert_text/get.py
new file mode 100644
index 0000000000..e8d0aa04e6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_alert_text/get.py
@@ -0,0 +1,73 @@
+from webdriver.error import NoSuchAlertException
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.sync import Poll
+
+
+def get_alert_text(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/alert/text".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_alert_text(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_alert_text(session)
+ assert_error(response, "no such alert")
+
+
+def test_no_user_prompt(session):
+ response = get_alert_text(session)
+ assert_error(response, "no such alert")
+
+
+def test_get_alert_text(session, inline):
+ session.url = inline("<script>window.alert('Hello');</script>")
+ response = get_alert_text(session)
+ assert_success(response)
+ assert isinstance(response.body, dict)
+ assert "value" in response.body
+ alert_text = response.body["value"]
+ assert isinstance(alert_text, str)
+ assert alert_text == "Hello"
+
+
+def test_get_confirm_text(session, inline):
+ session.url = inline("<script>window.confirm('Hello');</script>")
+ response = get_alert_text(session)
+ assert_success(response)
+ assert isinstance(response.body, dict)
+ assert "value" in response.body
+ confirm_text = response.body["value"]
+ assert isinstance(confirm_text, str)
+ assert confirm_text == "Hello"
+
+
+def test_get_prompt_text(session, inline):
+ session.url = inline("<script>window.prompt('Enter Your Name: ', 'Federer');</script>")
+ response = get_alert_text(session)
+ assert_success(response)
+ assert isinstance(response.body, dict)
+ assert "value" in response.body
+ prompt_text = response.body["value"]
+ assert isinstance(prompt_text, str)
+ assert prompt_text == "Enter Your Name: "
+
+
+# TODO: Add test for beforeunload?
+
+
+def test_unexpected_alert(session):
+ session.execute_script("setTimeout(function() { alert('Hello'); }, 100);")
+ wait = Poll(
+ session,
+ timeout=5,
+ ignored_exceptions=NoSuchAlertException,
+ message="No user prompt with text 'Hello' detected")
+ wait.until(lambda s: s.alert.text == "Hello")
+
+ response = get_alert_text(session)
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_computed_label/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_computed_label/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_computed_label/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_computed_label/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_computed_label/get.py
new file mode 100644
index 0000000000..c22bae5b50
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_computed_label/get.py
@@ -0,0 +1,88 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_computed_label(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/computedlabel".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_computed_label(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ result = get_computed_label(session, element.id)
+ assert_error(result, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_computed_label(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ result = get_computed_label(session, element.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ result = get_computed_label(session, element.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ response = get_computed_label(session, element.id)
+ assert_error(response, "stale element reference")
+
+
+@pytest.mark.parametrize("html,tag,label", [
+ ("<button>ok</button>", "button", "ok"),
+ ("<button aria-labelledby=\"one two\"></button><div id=one>ok</div><div id=two>go</div>", "button", "ok go"),
+ ("<button aria-label=foo>bar</button>", "button", "foo"),
+ ("<label><input> foo</label>", "input", "foo"),
+ ("<label for=b>foo<label><input id=b>", "input", "foo")])
+def test_get_computed_label(session, inline, html, tag, label):
+ session.url = inline(html)
+ element = session.find.css(tag, all=False)
+ result = get_computed_label(session, element.id)
+ assert_success(result, label)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_computed_role/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_computed_role/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_computed_role/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_computed_role/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_computed_role/get.py
new file mode 100644
index 0000000000..0990eecb90
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_computed_role/get.py
@@ -0,0 +1,86 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_computed_role(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/computedrole".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_computed_role(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ result = get_computed_role(session, element.id)
+ assert_error(result, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_computed_role(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ result = get_computed_role(session, element.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ result = get_computed_role(session, element.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ response = get_computed_role(session, element.id)
+ assert_error(response, "stale element reference")
+
+
+@pytest.mark.parametrize("html,tag,expected", [
+ ("<article>foo</article>", "article", "article"),
+ ("<input role=searchbox>", "input", "searchbox"),
+ ("<img role=button tabindex=0>", "img", "button")])
+def test_computed_roles(session, inline, html, tag, expected):
+ session.url = inline(html)
+ element = session.find.css(tag, all=False)
+ result = get_computed_role(session, element.id)
+ assert_success(result, expected)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_current_url/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_current_url/file.py b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/file.py
new file mode 100644
index 0000000000..ef6ae23835
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/file.py
@@ -0,0 +1,23 @@
+from tests.support import platform_name
+from tests.support.asserts import assert_success
+
+
+def get_current_url(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/url".format(**vars(session)))
+
+
+def test_get_current_url_file_protocol(session, server_config):
+ # tests that the browsing context remains the same
+ # when navigated privileged documents
+ path = server_config["doc_root"]
+ if platform_name == "windows":
+ # Convert the path into the format eg. /c:/foo/bar
+ path = "/{}".format(path.replace("\\", "/"))
+ url = u"file://{}".format(path)
+ session.url = url
+
+ response = get_current_url(session)
+ if response.status == 200 and response.body['value'].endswith('/'):
+ url += '/'
+ assert_success(response, url)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_current_url/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/get.py
new file mode 100644
index 0000000000..5819804f23
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/get.py
@@ -0,0 +1,73 @@
+import pytest
+
+from tests.support.asserts import assert_error, assert_success
+
+
+@pytest.fixture
+def doc(inline):
+ return inline("<p>frame")
+
+
+def get_current_url(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/url".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_current_url(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame, doc):
+ session.url = doc
+
+ response = get_current_url(session)
+ assert_success(response, doc)
+
+
+def test_get_current_url_matches_location(session, doc):
+ session.url = doc
+
+ response = get_current_url(session)
+ assert_success(response, doc)
+
+
+def test_get_current_url_payload(session):
+ session.start()
+
+ response = get_current_url(session)
+ value = assert_success(response)
+ assert isinstance(value, str)
+
+
+def test_get_current_url_special_pages(session):
+ session.url = "about:blank"
+
+ response = get_current_url(session)
+ assert_success(response, "about:blank")
+
+
+# TODO(ato): Test for http:// and https:// protocols.
+# We need to expose a fixture for accessing
+# documents served by wptserve in order to test this.
+
+
+def test_set_malformed_url(session):
+ response = session.transport.send(
+ "POST",
+ "session/%s/url" % session.session_id, {"url": "foo"})
+
+ assert_error(response, "invalid argument")
+
+
+def test_get_current_url_after_modified_location(session, doc):
+ session.url = doc
+
+ response = get_current_url(session)
+ assert_success(response, doc)
+
+ hash_doc = "{}#foo".format(doc)
+ session.url = hash_doc
+
+ response = get_current_url(session)
+ assert_success(response, hash_doc)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_current_url/iframe.py b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/iframe.py
new file mode 100644
index 0000000000..80a960ce8a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/iframe.py
@@ -0,0 +1,75 @@
+import pytest
+
+from tests.support.asserts import assert_success
+
+
+"""
+Tests that WebDriver can transcend site origins.
+
+Many modern browsers impose strict cross-origin checks,
+and WebDriver should be able to transcend these.
+
+Although an implementation detail, certain browsers
+also enforce process isolation based on site origin.
+This is known to sometimes cause problems for WebDriver implementations.
+"""
+
+
+@pytest.fixture
+def frame_doc(inline):
+ return inline("<p>frame")
+
+
+@pytest.fixture
+def one_frame_doc(inline, frame_doc):
+ return inline("<iframe src='%s'></iframe>" % frame_doc)
+
+
+@pytest.fixture
+def nested_frames_doc(inline, one_frame_doc):
+ return inline("<iframe src='%s'></iframe>" % one_frame_doc)
+
+
+def get_current_url(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/url".format(**vars(session)))
+
+
+def test_iframe(session, one_frame_doc):
+ top_level_doc = one_frame_doc
+ session.url = top_level_doc
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+ session.find.css("p", all=False)
+
+ response = get_current_url(session)
+ assert_success(response, top_level_doc)
+
+
+def test_nested_iframe(session, nested_frames_doc):
+ session.url = nested_frames_doc
+ top_level_doc = session.url
+
+ outer_frame = session.find.css("iframe", all=False)
+ session.switch_frame(outer_frame)
+
+ inner_frame = session.find.css("iframe", all=False)
+ session.switch_frame(inner_frame)
+ session.find.css("p", all=False)
+
+ response = get_current_url(session)
+ assert_success(response, top_level_doc)
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+def test_origin(session, inline, iframe, domain):
+ top_level_doc = inline(iframe("<p>frame", domain=domain))
+
+ session.url = top_level_doc
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+ session.find.css("p", all=False)
+
+ response = get_current_url(session)
+ assert_success(response, top_level_doc)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_current_url/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/user_prompts.py
new file mode 100644
index 0000000000..d657c18824
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_current_url/user_prompts.py
@@ -0,0 +1,111 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def get_current_url(session):
+ return session.transport.send("GET", "session/%s/url" % session.session_id)
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<p id=1>")
+ expected_url = session.url
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_current_url(session)
+ assert_success(response, expected_url)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<p id=1>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_current_url(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<p id=1>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_current_url(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/get.py
new file mode 100644
index 0000000000..0fcfd00c97
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/get.py
@@ -0,0 +1,167 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_element_attribute(session, element_id, attr):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/attribute/{attr}".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ attr=attr))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = get_element_attribute(session, element.id, "id")
+ assert_error(response, "no such window")
+ response = get_element_attribute(session, "foo", "id")
+ assert_error(response, "no such window")
+ session.window_handle = original_handle
+ response = get_element_attribute(session, element.id, "id")
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_element_attribute(session, "foo", "id")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = get_element_attribute(session, element.id, "id")
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_element_attribute(session, element.shadow_root.id, "id")
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = get_element_attribute(session, element.id, "id")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = get_element_attribute(session, element.id, "id")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = get_element_attribute(session, element.id, "id")
+ assert_error(result, "stale element reference")
+
+
+def test_normal(session, inline):
+ # 13.2 Step 5
+ session.url = inline("<input type=checkbox>")
+ element = session.find.css("input", all=False)
+ result = get_element_attribute(session, element.id, "input")
+ assert_success(result, None)
+
+ # Check we are not returning the property which will have a different value
+ assert session.execute_script("return document.querySelector('input').checked") is False
+ element.click()
+ assert session.execute_script("return document.querySelector('input').checked") is True
+ result = get_element_attribute(session, element.id, "input")
+ assert_success(result, None)
+
+
+@pytest.mark.parametrize("tag,attrs", [
+ ("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 test_boolean_attribute(session, inline, tag, attrs):
+ for attr in attrs:
+ session.url = inline("<{0} {1}>".format(tag, attr))
+ element = session.find.css(tag, all=False)
+ result = get_element_attribute(session, element.id, attr)
+ assert_success(result, "true")
+
+
+def test_global_boolean_attributes(session, inline):
+ session.url = inline("<p hidden>foo")
+ element = session.find.css("p", all=False)
+ result = get_element_attribute(session, element.id, "hidden")
+
+ assert_success(result, "true")
+
+ session.url = inline("<p>foo")
+ element = session.find.css("p", all=False)
+ result = get_element_attribute(session, element.id, "hidden")
+ assert_success(result, None)
+
+ session.url = inline("<p itemscope>foo")
+ element = session.find.css("p", all=False)
+ result = get_element_attribute(session, element.id, "itemscope")
+
+ assert_success(result, "true")
+
+ session.url = inline("<p>foo")
+ element = session.find.css("p", all=False)
+ result = get_element_attribute(session, element.id, "itemscope")
+ assert_success(result, None)
+
+
+@pytest.mark.parametrize("is_relative", [True, False], ids=["relative", "absolute"])
+def test_anchor_href(session, inline, url, is_relative):
+ href = "/foo.html" if is_relative else url("/foo.html")
+
+ session.url = inline("<a href='{}'>foo</a>".format(href))
+ element = session.find.css("a", all=False)
+
+ response = get_element_attribute(session, element.id, "href")
+ assert_success(response, href)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/user_prompts.py
new file mode 100644
index 0000000000..009cb1e5fa
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_attribute/user_prompts.py
@@ -0,0 +1,117 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def get_element_attribute(session, element, attr):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/attribute/{attr}".format(
+ session_id=session.session_id,
+ element_id=element,
+ attr=attr))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_attribute(session, element.id, "id")
+ assert_success(response, "foo")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_attribute(session, element.id, "id")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_attribute(session, element.id, "id")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/get.py
new file mode 100644
index 0000000000..1f6f571149
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/get.py
@@ -0,0 +1,107 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_element_css_value(session, element_id, prop):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/css/{prop}".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ prop=prop
+ )
+ )
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = get_element_css_value(session, element.id, "display")
+ assert_error(response, "no such window")
+ response = get_element_css_value(session, "foo", "bar")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = get_element_css_value(session, element.id, "display")
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_element_css_value(session, "foo", "bar")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = get_element_css_value(session, element.id, "display")
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_element_css_value(session, element.shadow_root.id, "display")
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = get_element_css_value(session, element.id, "display")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = get_element_css_value(session, element.id, "display")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = get_element_css_value(session, element.id, "display")
+ assert_error(result, "stale element reference")
+
+
+def test_property_name_value(session, inline):
+ session.url = inline("""<input style="display: block">""")
+ element = session.find.css("input", all=False)
+
+ result = get_element_css_value(session, element.id, "display")
+ assert_success(result, "block")
+
+
+def test_property_name_not_existent(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ result = get_element_css_value(session, element.id, "foo")
+ assert_success(result, "")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/user_prompts.py
new file mode 100644
index 0000000000..b1f9a3fb0a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_css_value/user_prompts.py
@@ -0,0 +1,120 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def get_element_css_value(session, element_id, prop):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/css/{prop}".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ prop=prop
+ )
+ )
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("""<input style="display: block">""")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_css_value(session, element.id, "display")
+ assert_success(response, "block")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("""<input style="display: block">""")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_css_value(session, element.id, "display")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("""<input style="display: block">""")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_css_value(session, element.id, "display")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_property/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_property/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_property/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_property/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_property/get.py
new file mode 100644
index 0000000000..fe354b4f2c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_property/get.py
@@ -0,0 +1,215 @@
+import pytest
+
+from webdriver import WebElement, WebFrame, ShadowRoot, WebWindow
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_element_property(session, element_id, prop):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/property/{prop}".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ prop=prop))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = get_element_property(session, element.id, "value")
+ assert_error(response, "no such window")
+ response = get_element_property(session, "foo", "id")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = get_element_property(session, element.id, "value")
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_element_property(session, "foo", "id")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = get_element_property(session, element.id, "id")
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_element_property(session, element.shadow_root.id, "id")
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = get_element_property(session, element.id, "id")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = get_element_property(session, element.id, "id")
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = get_element_property(session, element.id, "id")
+ assert_error(result, "stale element reference")
+
+
+def test_property_non_existent(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ response = get_element_property(session, element.id, "foo")
+ assert_success(response, None)
+ assert session.execute_script("return arguments[0].foo", args=(element,)) is None
+
+
+def test_content_attribute(session, inline):
+ session.url = inline("<input value=foobar>")
+ element = session.find.css("input", all=False)
+
+ response = get_element_property(session, element.id, "value")
+ assert_success(response, "foobar")
+
+
+def test_idl_attribute(session, inline):
+ session.url = inline("<input value=foo>")
+ element = session.find.css("input", all=False)
+ session.execute_script("""arguments[0].value = "bar";""", args=(element,))
+
+ response = get_element_property(session, element.id, "value")
+ assert_success(response, "bar")
+
+
+@pytest.mark.parametrize("js_primitive,py_primitive", [
+ ("\"foobar\"", "foobar"),
+ (42, 42),
+ ([], []),
+ ({}, {}),
+ ("null", None),
+ ("undefined", None),
+])
+def test_primitives(session, inline, js_primitive, py_primitive):
+ session.url = inline("""
+ <input>
+
+ <script>
+ const input = document.querySelector("input");
+ input.foobar = {js_primitive};
+ </script>
+ """.format(js_primitive=js_primitive))
+ element = session.find.css("input", all=False)
+
+ response = get_element_property(session, element.id, "foobar")
+ assert_success(response, py_primitive)
+
+
+def test_collection_dom_token_list(session, inline):
+ session.url = inline("""<div class="no cheese">""")
+ element = session.find.css("div", all=False)
+
+ response = get_element_property(session, element.id, "classList")
+ value = assert_success(response)
+
+ assert value == ["no", "cheese"]
+
+
+@pytest.mark.parametrize("js_primitive,py_primitive", [
+ ("\"foobar\"", "foobar"),
+ (42, 42),
+ ([], []),
+ ({}, {}),
+ ("null", None),
+ ("undefined", None),
+])
+def test_primitives_set_by_execute_script(session, inline, js_primitive, py_primitive):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+ session.execute_script("arguments[0].foobar = {}".format(js_primitive), args=(element,))
+
+ response = get_element_property(session, element.id, "foobar")
+ assert_success(response, py_primitive)
+
+
+@pytest.mark.parametrize("js_web_reference,py_web_reference", [
+ ("element", WebElement),
+ ("frame", WebFrame),
+ ("shadowRoot", ShadowRoot),
+ ("window", WebWindow),
+])
+def test_web_reference(session, get_test_page, js_web_reference, py_web_reference):
+ session.url = get_test_page()
+
+ session.execute_script("""
+ const parent = document.querySelector("body");
+ parent.__element = document.querySelector("div");
+ parent.__frame = document.querySelector("iframe").contentWindow;
+ parent.__shadowRoot = document.querySelector("custom-element").shadowRoot;
+ parent.__window = document.defaultView;
+ """)
+
+ elem = session.find.css("body", all=False)
+ response = get_element_property(session, elem.id, "__{}".format(js_web_reference))
+ value = assert_success(response)
+
+ assert isinstance(value, dict)
+ assert py_web_reference.identifier in value
+ assert isinstance(value[py_web_reference.identifier], str)
+
+
+def test_mutated_element(session, inline):
+ session.url = inline("<input type=checkbox>")
+ element = session.find.css("input", all=False)
+ element.click()
+
+ checked = session.execute_script("""
+ return arguments[0].hasAttribute('checked')
+ """, args=(element,))
+ assert checked is False
+
+ response = get_element_property(session, element.id, "checked")
+ assert_success(response, True)
+
+
+@pytest.mark.parametrize("is_relative", [True, False], ids=["relative", "absolute"])
+def test_anchor_href(session, inline, url, is_relative):
+ href = "/foo.html" if is_relative else url("/foo.html")
+
+ session.url = inline("<a href='{}'>foo</a>".format(href))
+ element = session.find.css("a", all=False)
+
+ response = get_element_property(session, element.id, "href")
+ assert_success(response, url("/foo.html"))
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_property/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_property/user_prompts.py
new file mode 100644
index 0000000000..e5e7694786
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_property/user_prompts.py
@@ -0,0 +1,115 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def get_element_property(session, element_id, name):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/property/{name}".format(
+ session_id=session.session_id, element_id=element_id, name=name))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_property(session, element.id, "id")
+ assert_success(response, "foo")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_property(session, element.id, "id")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_property(session, element.id, "id")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/get.py
new file mode 100644
index 0000000000..959ccc455e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/get.py
@@ -0,0 +1,99 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import element_rect
+
+
+def get_element_rect(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/rect".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ )
+ )
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = get_element_rect(session, element.id)
+ assert_error(response, "no such window")
+ response = get_element_rect(session, "foo")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = get_element_rect(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_element_rect(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = get_element_rect(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_element_rect(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = get_element_rect(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = get_element_rect(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = get_element_rect(session, element.id)
+ assert_error(result, "stale element reference")
+
+
+def test_basic(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ result = get_element_rect(session, element.id)
+ assert_success(result, element_rect(session, element))
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/user_prompts.py
new file mode 100644
index 0000000000..2013160338
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_rect/user_prompts.py
@@ -0,0 +1,120 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+from tests.support.helpers import element_rect
+
+
+def get_element_rect(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/rect".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ )
+ )
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_rect(session, element.id)
+ assert_success(response, element_rect(session, element))
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_rect(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_rect(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/get.py
new file mode 100644
index 0000000000..25e68c1bba
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/get.py
@@ -0,0 +1,102 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def get_shadow_root(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/shadow".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "no such window")
+ response = get_shadow_root(session, "foo")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_shadow_root(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("custom-element", as_frame=as_frame)
+
+ result = get_shadow_root(session, element.id)
+ assert_error(result, "stale element reference")
+
+
+def test_get_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ host_element = session.find.css("custom-element", all=False)
+
+ response = get_shadow_root(session, host_element.id)
+ value = assert_success(response)
+ assert isinstance(value, dict)
+ assert "shadow-6066-11e4-a52e-4f735466cecf" in value
+
+ expected_host = session.execute_script("""
+ return arguments[0].shadowRoot.host
+ """, args=(host_element,))
+
+ assert_same_element(session, host_element, expected_host)
+
+
+def test_no_shadow_root(session, inline):
+ session.url = inline("<div><p>no shadow root</p></div>")
+ element = session.find.css("div", all=False)
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "no such shadow root")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/user_prompts.py
new file mode 100644
index 0000000000..5b991bac26
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_shadow_root/user_prompts.py
@@ -0,0 +1,117 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def get_shadow_root(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/shadow".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = get_test_page()
+ element = session.find.css("custom-element", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_shadow_root(session, element.id)
+ value = assert_success(response)
+ assert isinstance(value, dict)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = get_test_page()
+ element = session.find.css("custom-element", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, get_test_page):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = get_test_page()
+ element = session.find.css("custom-element", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_shadow_root(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/get.py
new file mode 100644
index 0000000000..d8bb3acc50
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/get.py
@@ -0,0 +1,95 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_element_tag_name(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/name".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = get_element_tag_name(session, element.id)
+ assert_error(response, "no such window")
+ response = get_element_tag_name(session, "foo")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = get_element_tag_name(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_element_tag_name(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = get_element_tag_name(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_element_tag_name(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = get_element_tag_name(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = get_element_tag_name(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = get_element_tag_name(session, element.id)
+ assert_error(result, "stale element reference")
+
+
+def test_get_element_tag_name(session, inline):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("input", all=False)
+
+ result = get_element_tag_name(session, element.id)
+ assert_success(result, "input")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/user_prompts.py
new file mode 100644
index 0000000000..89697d0ad6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_tag_name/user_prompts.py
@@ -0,0 +1,114 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def get_element_tag_name(session, element_id):
+ return session.transport.send("GET", "session/{session_id}/element/{element_id}/name".format(
+ session_id=session.session_id, element_id=element_id))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_tag_name(session, element.id)
+ assert_success(response, "input")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_tag_name(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_tag_name(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_text/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py
new file mode 100644
index 0000000000..924a4e8d79
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/get.py
@@ -0,0 +1,145 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_element_text(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/text".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = get_element_text(session, element.id)
+ assert_error(response, "no such window")
+ response = get_element_text(session, "foo")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = get_element_text(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_element_text(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = get_element_text(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = get_element_text(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = get_element_text(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = get_element_text(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ response = get_element_text(session, element.id)
+ assert_error(response, "stale element reference")
+
+
+def test_getting_text_of_a_non_existant_element_is_an_error(session, inline):
+ session.url = inline("""<body>Hello world</body>""")
+
+ result = get_element_text(session, "foo")
+ assert_error(result, "no such element")
+
+
+def test_read_element_text(session, inline):
+ session.url = inline("Before f<span id='id'>oo</span> after")
+ element = session.find.css("#id", all=False)
+
+ result = get_element_text(session, element.id)
+ assert_success(result, "oo")
+
+
+@pytest.mark.parametrize("text, inner_html, expected", [
+ ("cheese", "<slot><span>foo</span>bar</slot>", "cheese"),
+ ("cheese", "<slot><span>foo</span></slot>bar", "cheesebar"),
+ ("cheese", "<slot><span style=\"display: none\">foo</span>bar</slot>", "cheese"),
+ ("", "<slot><span>foo</span>bar</slot>", "foobar"),
+ ("", "<slot><span>foo</span></slot>bar", "foobar"),
+ ("", "<slot><span style='display: none'>foo</span>bar</slot>", "bar"),
+], ids=[
+ "custom visible",
+ "custom outside",
+ "custom hidden",
+ "default visible",
+ "default outside",
+ "default hidden",
+])
+def test_shadow_root_slot(session, inline, text, inner_html, expected):
+ session.url = inline(f"""
+ <test-container>{text}</test-container>
+ <script>
+ class TestContainer extends HTMLElement {{
+ connectedCallback() {{
+ const shadow = this.attachShadow({{ mode: "open" }});
+ shadow.innerHTML = "{inner_html}";
+ }}
+ }}
+
+ customElements.define("test-container", TestContainer);
+ </script>
+ """)
+
+ element = session.find.css("test-container", all=False)
+
+ result = get_element_text(session, element.id)
+ assert_success(result, expected)
+
+
+def test_pretty_print_xml(session, inline):
+ session.url = inline("<xml><foo>che<bar>ese</bar></foo></xml>", doctype="xml")
+
+ elem = session.find.css("foo", all=False)
+ assert elem.text == "cheese"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_element_text/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/user_prompts.py
new file mode 100644
index 0000000000..9f0bb386cd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_element_text/user_prompts.py
@@ -0,0 +1,116 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def get_element_text(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/text".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<p id=foo>bar</p>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_text(session, element.id)
+ assert_success(response, "bar")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<p id=foo>bar</p>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_text(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<p id=foo>bar</p>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_element_text(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py
new file mode 100644
index 0000000000..41426532ef
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/get.py
@@ -0,0 +1,144 @@
+import pytest
+
+from datetime import datetime, timedelta
+
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import clear_all_cookies
+
+
+def get_named_cookie(session, name):
+ return session.transport.send(
+ "GET", "session/{session_id}/cookie/{name}".format(
+ session_id=session.session_id,
+ name=name))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_named_cookie(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_named_cookie(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_get_named_session_cookie(session, url):
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+ session.execute_script("document.cookie = 'foo=bar'")
+
+ result = get_named_cookie(session, "foo")
+ cookie = assert_success(result)
+ assert isinstance(cookie, dict)
+
+ # table for cookie conversion
+ # https://w3c.github.io/webdriver/#dfn-table-for-cookie-conversion
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "path" in cookie
+ assert isinstance(cookie["path"], str)
+ assert "domain" in cookie
+ assert isinstance(cookie["domain"], str)
+ assert "secure" in cookie
+ assert isinstance(cookie["secure"], bool)
+ assert "httpOnly" in cookie
+ assert isinstance(cookie["httpOnly"], bool)
+ if "expiry" in cookie:
+ assert cookie.get("expiry") is None
+ assert "sameSite" in cookie
+ assert isinstance(cookie["sameSite"], str)
+
+ assert cookie["name"] == "foo"
+ assert cookie["value"] == "bar"
+
+
+def test_get_named_cookie(session, url):
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ # same formatting as Date.toUTCString() in javascript
+ utc_string_format = "%a, %d %b %Y %H:%M:%S"
+ a_day_from_now = (datetime.utcnow() + timedelta(days=1)).strftime(utc_string_format)
+ session.execute_script("document.cookie = 'foo=bar;expires=%s'" % a_day_from_now)
+
+ result = get_named_cookie(session, "foo")
+ cookie = assert_success(result)
+ assert isinstance(cookie, dict)
+
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "expiry" in cookie
+ assert isinstance(cookie["expiry"], int)
+ assert "sameSite" in cookie
+ assert isinstance(cookie["sameSite"], str)
+
+ assert cookie["name"] == "foo"
+ assert cookie["value"] == "bar"
+ # convert from seconds since epoch
+ assert datetime.utcfromtimestamp(
+ cookie["expiry"]).strftime(utc_string_format) == a_day_from_now
+
+
+def test_duplicated_cookie(session, url, server_config, inline):
+ new_cookie = {
+ "name": "hello",
+ "value": "world",
+ "domain": server_config["browser_host"],
+ "path": "/",
+ "http_only": False,
+ "secure": False
+ }
+
+ session.url = url("/common/blank.html")
+ clear_all_cookies(session)
+
+ session.set_cookie(**new_cookie)
+ session.url = inline("""
+ <script>
+ document.cookie = '{name}=newworld; domain={domain}; path=/';
+ </script>""".format(
+ name=new_cookie["name"],
+ domain=server_config["browser_host"]))
+
+ result = get_named_cookie(session, new_cookie["name"])
+ cookie = assert_success(result)
+ assert isinstance(cookie, dict)
+
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "sameSite" in cookie
+ assert isinstance(cookie["sameSite"], str)
+
+ assert cookie["name"] == new_cookie["name"]
+ assert cookie["value"] == "newworld"
+
+
+@pytest.mark.parametrize("same_site", ["None", "Lax", "Strict"])
+def test_get_cookie_with_same_site_flag(session, url, same_site):
+ session.url = url("/common/blank.html", protocol="https")
+ clear_all_cookies(session)
+
+ session.execute_script("document.cookie = 'foo=bar;Secure;SameSite=%s'" % same_site)
+
+ result = get_named_cookie(session, "foo")
+ cookie = assert_success(result)
+ assert isinstance(cookie, dict)
+
+ assert "name" in cookie
+ assert isinstance(cookie["name"], str)
+ assert "value" in cookie
+ assert isinstance(cookie["value"], str)
+ assert "sameSite" in cookie
+ assert isinstance(cookie["sameSite"], str)
+
+ assert cookie["name"] == "foo"
+ assert cookie["value"] == "bar"
+ assert cookie["sameSite"] == same_site
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/user_prompts.py
new file mode 100644
index 0000000000..f1669d6c99
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_named_cookie/user_prompts.py
@@ -0,0 +1,116 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def get_named_cookie(session, name):
+ return session.transport.send(
+ "GET", "session/{session_id}/cookie/{name}".format(
+ session_id=session.session_id,
+ name=name))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_named_cookie(session, "foo")
+ cookie = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert cookie["name"] == "foo"
+ assert cookie["value"] == "bar"
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_named_cookie(session, "foo")
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, create_cookie):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_cookie("foo", value="bar", path="/common/blank.html")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_named_cookie(session, "foo")
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_page_source/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_page_source/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_page_source/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_page_source/source.py b/testing/web-platform/tests/webdriver/tests/classic/get_page_source/source.py
new file mode 100644
index 0000000000..cc4e208835
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_page_source/source.py
@@ -0,0 +1,25 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_page_source(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/source".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_page_source(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_page_source(session)
+ assert_error(response, "no such window")
+
+
+def test_source_matches_outer_html(session, inline):
+ session.url = inline("<html><head><title>Cheese</title><body>Peas")
+
+ expected = session.execute_script("return document.documentElement.outerHTML")
+
+ response = get_page_source(session)
+ assert_success(response, expected)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_page_source/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_page_source/user_prompts.py
new file mode 100644
index 0000000000..13cb31595e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_page_source/user_prompts.py
@@ -0,0 +1,112 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def get_page_source(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/source".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<div/>")
+ expected = session.execute_script("return document.documentElement.outerHTML")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_page_source(session)
+ assert_success(response, expected)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<div/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_page_source(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<div/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_page_source(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_timeouts/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_timeouts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_timeouts/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_timeouts/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_timeouts/get.py
new file mode 100644
index 0000000000..aa02c0990e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_timeouts/get.py
@@ -0,0 +1,34 @@
+from tests.support.asserts import assert_success
+
+
+def get_timeouts(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/timeouts".format(**vars(session)))
+
+
+def test_get_timeouts(session):
+ response = get_timeouts(session)
+
+ assert_success(response)
+ assert "value" in response.body
+ assert isinstance(response.body["value"], dict)
+
+ value = response.body["value"]
+ assert "script" in value
+ assert "implicit" in value
+ assert "pageLoad" in value
+
+ assert isinstance(value["script"], int)
+ assert isinstance(value["implicit"], int)
+ assert isinstance(value["pageLoad"], int)
+
+
+def test_get_new_timeouts(session):
+ session.timeouts.script = 60
+ session.timeouts.implicit = 1
+ session.timeouts.page_load = 200
+ response = get_timeouts(session)
+ assert_success(response)
+ assert response.body["value"]["script"] == 60000
+ assert response.body["value"]["implicit"] == 1000
+ assert response.body["value"]["pageLoad"] == 200000
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_title/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_title/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_title/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_title/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_title/get.py
new file mode 100644
index 0000000000..e696ec3403
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_title/get.py
@@ -0,0 +1,56 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_title(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/title".format(**vars(session)))
+
+
+def test_payload(session):
+ session.start()
+
+ response = get_title(session)
+ value = assert_success(response)
+ assert isinstance(value, str)
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_title(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame, inline):
+ session.url = inline("<title>Foo</title>")
+
+ response = get_title(session)
+ assert_success(response, "Foo")
+
+
+def test_with_duplicated_title(session, inline):
+ session.url = inline("<title>First</title><title>Second</title>")
+
+ result = get_title(session)
+ assert_success(result, "First")
+
+
+def test_without_title(session, inline):
+ session.url = inline("<h2>Hello</h2>")
+
+ result = get_title(session)
+ assert_success(result, "")
+
+
+def test_after_modification(session, inline):
+ session.url = inline("<title>Initial</title><h2>Hello</h2>")
+ session.execute_script("document.title = 'Updated'")
+
+ result = get_title(session)
+ assert_success(result, "Updated")
+
+
+def test_strip_and_collapse(session, inline):
+ document = "<title> a b\tc\nd\t \n e\t\n </title><h2>Hello</h2>"
+ session.url = inline(document)
+
+ result = get_title(session)
+ assert_success(result, "a b c d e")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_title/iframe.py b/testing/web-platform/tests/webdriver/tests/classic/get_title/iframe.py
new file mode 100644
index 0000000000..9c5ab0b595
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_title/iframe.py
@@ -0,0 +1,80 @@
+import pytest
+
+from tests.support.asserts import assert_success
+
+
+"""
+Tests that WebDriver can transcend site origins.
+
+Many modern browsers impose strict cross-origin checks,
+and WebDriver should be able to transcend these.
+
+Although an implementation detail, certain browsers
+also enforce process isolation based on site origin.
+This is known to sometimes cause problems for WebDriver implementations.
+"""
+
+
+@pytest.fixture
+def frame_doc(inline):
+ return inline("<title>cheese</title><p>frame")
+
+
+@pytest.fixture
+def one_frame_doc(inline, frame_doc):
+ return inline("<title>bar</title><iframe src='%s'></iframe>" % frame_doc)
+
+
+@pytest.fixture
+def nested_frames_doc(inline, one_frame_doc):
+ return inline("<title>foo</title><iframe src='%s'></iframe>" % one_frame_doc)
+
+
+def get_title(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/title".format(**vars(session)))
+
+
+def test_no_iframe(session, inline):
+ session.url = inline("<title>Foobar</title><h2>Hello</h2>")
+
+ result = get_title(session)
+ assert_success(result, "Foobar")
+
+
+def test_iframe(session, one_frame_doc):
+ session.url = one_frame_doc
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+ session.find.css("p", all=False)
+
+ response = get_title(session)
+ assert_success(response, "bar")
+
+
+def test_nested_iframe(session, nested_frames_doc):
+ session.url = nested_frames_doc
+
+ outer_frame = session.find.css("iframe", all=False)
+ session.switch_frame(outer_frame)
+
+ inner_frame = session.find.css("iframe", all=False)
+ session.switch_frame(inner_frame)
+ session.find.css("p", all=False)
+
+ response = get_title(session)
+ assert_success(response, "foo")
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+def test_origin(session, inline, iframe, domain):
+ session.url = inline("<title>foo</title>{}".format(
+ iframe("<title>bar</title><p>frame", domain=domain)))
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+ session.find.css("p", all=False)
+
+ response = get_title(session)
+ assert_success(response, "foo")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_title/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_title/user_prompts.py
new file mode 100644
index 0000000000..0fd51e46f3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_title/user_prompts.py
@@ -0,0 +1,134 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def get_title(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/title".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<title>Foo</title>")
+ expected_title = session.title
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_title(session)
+ assert_success(response, expected_title)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<title>Foo</title>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_title(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<title>Foo</title>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_title(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+# The behavior of the `window.print` function is platform-dependent and may not
+# trigger the creation of a dialog at all. Therefore, this test should only be
+# run in contexts that support the dialog (a condition that may not be
+# determined automatically).
+# def test_title_with_non_simple_dialog(session, inline):
+# document = "<title>With non-simple dialog</title><h2>Hello</h2>"
+# spawn = """
+# var done = arguments[0];
+# setTimeout(function() {
+# done();
+# }, 0);
+# setTimeout(function() {
+# window['print']();
+# }, 0);
+# """
+# session.url = inline(document)
+# session.execute_async_script(spawn)
+#
+# result = get_title(session)
+# assert_error(result, "unexpected alert open")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/get.py
new file mode 100644
index 0000000000..922915f2dc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/get.py
@@ -0,0 +1,38 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_window_handle(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/window".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_window_handle(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_window_handle(session)
+ assert_success(response, session.window_handle)
+
+
+def test_basic(session):
+ response = get_window_handle(session)
+ assert_success(response, session.window_handle)
+
+
+def test_navigation_with_coop_headers(session, url):
+ base_path = ("/webdriver/tests/support/html/subframe.html" +
+ "?pipe=header(Cross-Origin-Opener-Policy,same-origin)")
+
+ session.url = url(base_path, protocol="https")
+ response = get_window_handle(session)
+ first_handle = assert_success(response)
+
+ # navigating to another domain with COOP headers will force a process change
+ # in most browsers
+ session.url = url(base_path, protocol="https", domain="alt")
+ response = get_window_handle(session)
+ second_handle = assert_success(response)
+
+ assert first_handle == second_handle
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/user_prompts.py
new file mode 100644
index 0000000000..0bd660cfa1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_handle/user_prompts.py
@@ -0,0 +1,61 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_success
+
+
+def get_window_handle(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/window".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ window_handle = session.window_handle
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_window_handle(session)
+ assert_success(response, window_handle)
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_accept(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_accept_and_notify(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_dismiss(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_dismiss_and_notify(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_default(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/get.py
new file mode 100644
index 0000000000..8f4361e30c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/get.py
@@ -0,0 +1,37 @@
+from tests.support.asserts import assert_success
+
+
+def get_window_handles(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/window/handles".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_window_handles(session)
+ assert_success(response, session.handles)
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_window_handles(session)
+ assert_success(response, session.handles)
+
+
+def test_single_window(session):
+ response = get_window_handles(session)
+ value = assert_success(response)
+
+ assert len(value) == 1
+ assert value == session.handles
+ assert value[0] == session.window_handle
+
+
+def test_multiple_windows(session):
+ original_handle = session.window_handle
+ new_handle = session.new_window()
+
+ response = get_window_handles(session)
+ value = assert_success(response)
+
+ assert len(value) == 2
+ assert original_handle in value
+ assert new_handle in value
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/user_prompts.py
new file mode 100644
index 0000000000..217e9849b4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_handles/user_prompts.py
@@ -0,0 +1,61 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_success
+
+
+def get_window_handles(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/window/handles".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ window_handles = session.handles
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_window_handles(session)
+ assert_success(response, window_handles)
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_accept(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_accept_and_notify(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_dismiss(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_dismiss_and_notify(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_default(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/get.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/get.py
new file mode 100644
index 0000000000..f7592a30e0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/get.py
@@ -0,0 +1,31 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def get_window_rect(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/window/rect".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = get_window_rect(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = get_window_rect(session)
+ assert_success(response)
+
+
+def test_payload(session):
+ expected = session.execute_script("""return {
+ x: window.screenX,
+ y: window.screenY,
+ width: window.outerWidth,
+ height: window.outerHeight
+ }""")
+
+ response = get_window_rect(session)
+ value = assert_success(response)
+
+ assert isinstance(value, dict)
+ assert value == expected
diff --git a/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/user_prompts.py
new file mode 100644
index 0000000000..37c8da6bd3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/get_window_rect/user_prompts.py
@@ -0,0 +1,113 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def get_window_rect(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/window/rect".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ original_rect = session.window.rect
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_window_rect(session)
+ assert_success(response, original_rect)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ original_rect = session.window.rect
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_window_rect(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.window.rect == original_rect
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = get_window_rect(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/idlharness.window.js b/testing/web-platform/tests/webdriver/tests/classic/idlharness.window.js
new file mode 100644
index 0000000000..e92e151d89
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/idlharness.window.js
@@ -0,0 +1,16 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+// https://w3c.github.io/webdriver/
+
+"use strict";
+
+idl_test(
+ ["webdriver"],
+ ["html"],
+ idl_array => {
+ idl_array.add_objects({
+ Navigator: ["navigator"]
+ });
+ }
+);
diff --git a/testing/web-platform/tests/webdriver/tests/classic/interface/interface.py b/testing/web-platform/tests/webdriver/tests/classic/interface/interface.py
new file mode 100644
index 0000000000..6a7afcd263
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/interface/interface.py
@@ -0,0 +1,2 @@
+def test_navigator_webdriver_active(session):
+ assert session.execute_script("return navigator.webdriver") is True
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py
new file mode 100644
index 0000000000..24fc85fdad
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/enabled.py
@@ -0,0 +1,171 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def is_element_enabled(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/enabled".format(
+ session_id=session.session_id,
+ element_id=element_id
+ )
+ )
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+ response = is_element_enabled(session, element.id)
+ assert_error(response, "no such window")
+ response = is_element_enabled(session, "foo")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = is_element_enabled(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = is_element_enabled(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = is_element_enabled(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = is_element_enabled(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = is_element_enabled(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("input#text", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = is_element_enabled(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = is_element_enabled(session, element.id)
+ assert_error(result, "stale element reference")
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_form_control_disabled(session, inline, element):
+ session.url = inline("<{} disabled/>".format(element))
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, False)
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_form_control_enabled(session, inline, element):
+ session.url = inline("<{}/>".format(element))
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, True)
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_fieldset_disabled_descendant(session, inline, element):
+ session.url = inline("<fieldset disabled><{}/></fieldset>".format(element))
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, False)
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_fieldset_enabled_descendant(session, inline, element):
+ session.url = inline("<fieldset><{}/></fieldset>".format(element))
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, True)
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_fieldset_disabled_descendant_legend(session, inline, element):
+ session.url = inline("<fieldset disabled><legend><{}/></legend></fieldset>".format(element))
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, True)
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_fieldset_enabled_descendant_legend(session, inline, element):
+ session.url = inline("<fieldset><legend><{}/></legend></fieldset>".format(element))
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, True)
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_xhtml_form_control_disabled(session, inline, element):
+ session.url = inline("""<{} disabled="disabled"/>""".format(element),
+ doctype="xhtml")
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, False)
+
+
+@pytest.mark.parametrize("element", ["button", "input", "select", "textarea"])
+def test_xhtml_form_control_enabled(session, inline, element):
+ session.url = inline("""<{}/>""".format(element), doctype="xhtml")
+ element = session.find.css(element, all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, True)
+
+
+def test_xml_always_not_enabled(session, inline):
+ session.url = inline("""<note></note>""", doctype="xml")
+ element = session.find.css("note", all=False)
+
+ result = is_element_enabled(session, element.id)
+ assert_success(result, False)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py
new file mode 100644
index 0000000000..5dd7d582bd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_enabled/user_prompts.py
@@ -0,0 +1,119 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_dialog_handled, assert_success
+
+
+def is_element_enabled(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/enabled".format(
+ session_id=session.session_id,
+ element_id=element_id
+ )
+ )
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input id=foo disabled>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = is_element_enabled(session, element.id)
+ assert_success(response, False)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input id=foo disabled>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = is_element_enabled(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input id=foo disabled>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = is_element_enabled(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/selected.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/selected.py
new file mode 100644
index 0000000000..bf650de3e2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/selected.py
@@ -0,0 +1,138 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+
+
+@pytest.fixture
+def check_doc():
+ return """
+ <input id=checked type=checkbox checked>
+ <input id=notChecked type=checkbox>
+ """
+
+
+@pytest.fixture
+def option_doc():
+ return """
+ <select>
+ <option id=notSelected>r-
+ <option id=selected selected>r+
+ </select>
+ """
+
+
+def is_element_selected(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/selected".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ original_handle, element = closed_window
+
+ response = is_element_selected(session, element.id)
+ assert_error(response, "no such window")
+ response = is_element_selected(session, "foo")
+ assert_error(response, "no such window")
+
+ session.window_handle = original_handle
+ response = is_element_selected(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = is_element_selected(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = is_element_selected(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = is_element_selected(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = is_element_selected(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("input#text", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = is_element_selected(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, check_doc, as_frame):
+ element = stale_element("input#checkbox", as_frame=as_frame)
+
+ result = is_element_selected(session, element.id)
+ assert_error(result, "stale element reference")
+
+
+def test_element_checked(session, inline, check_doc):
+ session.url = inline(check_doc)
+ element = session.find.css("#checked", all=False)
+
+ result = is_element_selected(session, element.id)
+ assert_success(result, True)
+
+
+def test_checkbox_not_selected(session, inline, check_doc):
+ session.url = inline(check_doc)
+ element = session.find.css("#notChecked", all=False)
+
+ result = is_element_selected(session, element.id)
+ assert_success(result, False)
+
+
+def test_element_selected(session, inline, option_doc):
+ session.url = inline(option_doc)
+ element = session.find.css("#selected", all=False)
+
+ result = is_element_selected(session, element.id)
+ assert_success(result, True)
+
+
+def test_element_not_selected(session, inline, option_doc):
+ session.url = inline(option_doc)
+ element = session.find.css("#notSelected", all=False)
+
+ result = is_element_selected(session, element.id)
+ assert_success(result, False)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/user_prompts.py
new file mode 100644
index 0000000000..96da2c08bd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/is_element_selected/user_prompts.py
@@ -0,0 +1,117 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_dialog_handled, assert_success
+
+
+def is_element_selected(session, element_id):
+ return session.transport.send(
+ "GET", "session/{session_id}/element/{element_id}/selected".format(
+ session_id=session.session_id,
+ element_id=element_id))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input id=foo type=checkbox checked>")
+ element = session.find.css("#foo", all=False)
+ element.send_keys("foo")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = is_element_selected(session, element.id)
+ assert_success(response, True)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input id=foo type=checkbox checked>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = is_element_selected(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input id=foo type=checkbox checked>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = is_element_selected(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/maximize_window/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/maximize_window/maximize.py b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/maximize.py
new file mode 100644
index 0000000000..3a20a0d558
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/maximize.py
@@ -0,0 +1,113 @@
+# META: timeout=long
+
+# Longer timeout required due to a bug in Chrome:
+# https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import (
+ document_hidden,
+ is_fullscreen,
+ is_maximized,
+)
+
+
+def maximize(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/maximize".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = maximize(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = maximize(session)
+ assert_success(response)
+
+
+def test_response_payload(session):
+ assert not is_maximized(session)
+
+ response = maximize(session)
+ value = assert_success(response, session.window.rect)
+
+ assert is_maximized(session)
+
+ assert isinstance(value, dict)
+ assert isinstance(value.get("x"), int)
+ assert isinstance(value.get("y"), int)
+ assert isinstance(value.get("width"), int)
+ assert isinstance(value.get("height"), int)
+
+
+def test_fully_exit_fullscreen(session):
+ assert not is_maximized(session)
+
+ session.window.fullscreen()
+ assert is_fullscreen(session)
+
+ response = maximize(session)
+ assert_success(response, session.window.rect)
+
+ assert is_maximized(session)
+ assert not document_hidden(session)
+
+
+def test_restore_from_minimized(session):
+ assert not is_maximized(session)
+
+ session.window.minimize()
+ assert document_hidden(session)
+ assert not is_maximized(session)
+
+ response = maximize(session)
+ assert_success(response, session.window.rect)
+
+ assert is_maximized(session)
+ assert not document_hidden(session)
+
+
+def test_maximize_from_normal_window(session):
+ assert not is_maximized(session)
+
+ response = maximize(session)
+ assert_success(response, session.window.rect)
+
+ assert is_maximized(session)
+ assert not document_hidden(session)
+
+
+def test_maximize_with_window_already_at_maximum_size(session, available_screen_size):
+ assert not is_maximized(session)
+
+ # Resize the window to the maximum available size.
+ session.window.size = available_screen_size
+ assert session.window.size == available_screen_size
+
+ # In certain window managers a window extending to the full available
+ # dimensions of the screen may not imply that the window is maximised,
+ # since this is often a special state. If a remote end expects a DOM
+ # resize event, this may not fire if the window has already reached
+ # its expected dimensions.
+ response = maximize(session)
+ assert_success(response, session.window.rect)
+
+ assert is_maximized(session)
+ assert not document_hidden(session)
+
+
+def test_maximize_twice_is_idempotent(session):
+ assert not is_maximized(session)
+
+ first_response = maximize(session)
+ assert_success(first_response, session.window.rect)
+
+ assert is_maximized(session)
+ assert not document_hidden(session)
+
+ second_response = maximize(session)
+ assert_success(second_response, session.window.rect)
+
+ assert is_maximized(session)
+ assert not document_hidden(session)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/maximize_window/stress.py b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/stress.py
new file mode 100644
index 0000000000..23f048e846
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/stress.py
@@ -0,0 +1,45 @@
+# META: timeout=long
+
+# Longer timeout required due to a bug in Chrome:
+# https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+
+import time
+
+import pytest
+
+from tests.support.asserts import assert_success
+
+
+def maximize_window(session):
+ response = session.transport.send(
+ "POST", "session/{session_id}/window/maximize".format(**vars(session)))
+ rect = assert_success(response)
+ return (rect["width"], rect["height"])
+
+
+@pytest.mark.parametrize("i", range(5))
+def test_stress(session, i):
+ """
+ Without defining the heuristics of each platform WebDriver runs on,
+ the best we can do is to test that maximization occurs synchronously.
+
+ Not all systems and window managers support maximizing the window,
+ but they are expected to do their best. The minimum requirement
+ is that the maximized window is larger than its original size.
+
+ To ensure the maximization happened synchronously, we test
+ that the size hasn't changed after a short amount of time,
+ using a thread suspend. This is not ideal, but the best we
+ can do given the level of platform ambiguity implied by WebDriver.
+ """
+ session.window.size = (100, 100)
+ session.window.position = (0, 0)
+ original_size = session.window.size
+
+ size_after_maximize = maximize_window(session)
+ assert size_after_maximize > original_size
+
+ t_end = time.time() + 3
+ while time.time() < t_end:
+ assert session.window.size == size_after_maximize
+ time.sleep(.1)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/maximize_window/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/user_prompts.py
new file mode 100644
index 0000000000..032edc893a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/maximize_window/user_prompts.py
@@ -0,0 +1,117 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def maximize(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/maximize".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ original_size = session.window.size
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = maximize(session)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.window.size != original_size
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ original_size = session.window.size
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = maximize(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.window.size == original_size
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ original_size = session.window.size
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = maximize(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.window.size == original_size
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/minimize_window/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/minimize_window/minimize.py b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/minimize.py
new file mode 100644
index 0000000000..2ad5333ec5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/minimize.py
@@ -0,0 +1,83 @@
+# META: timeout=long
+
+# Longer timeout required due to a bug in Chrome:
+# https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import document_hidden, is_fullscreen, is_maximized
+
+
+def minimize(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/minimize".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = minimize(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = minimize(session)
+ assert_success(response)
+
+
+def test_response_payload(session):
+ assert not document_hidden(session)
+
+ response = minimize(session)
+ value = assert_success(response, session.window.rect)
+
+ assert document_hidden(session)
+
+ assert isinstance(value, dict)
+ assert isinstance(value.get("x"), int)
+ assert isinstance(value.get("y"), int)
+ assert isinstance(value.get("width"), int)
+ assert isinstance(value.get("height"), int)
+
+
+def test_restore_from_fullscreen(session):
+ assert not document_hidden(session)
+
+ session.window.fullscreen()
+ assert is_fullscreen(session)
+ assert not document_hidden(session)
+
+ response = minimize(session)
+ assert_success(response, session.window.rect)
+ assert not is_fullscreen(session)
+ assert document_hidden(session)
+
+
+def test_restore_from_maximized(session):
+ assert not document_hidden(session)
+
+ session.window.maximize()
+ assert is_maximized(session)
+ assert not document_hidden(session)
+
+ response = minimize(session)
+ assert_success(response, session.window.rect)
+ assert not is_maximized(session)
+ assert document_hidden(session)
+
+
+def test_minimize_from_normal_window(session):
+ assert not document_hidden(session)
+
+ response = minimize(session)
+ assert_success(response, session.window.rect)
+ assert document_hidden(session)
+
+
+def test_minimize_twice_is_idempotent(session):
+ assert not document_hidden(session)
+
+ first_response = minimize(session)
+ assert_success(first_response, session.window.rect)
+ assert document_hidden(session)
+
+ second_response = minimize(session)
+ assert_success(second_response, session.window.rect)
+ assert document_hidden(session)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/minimize_window/stress.py b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/stress.py
new file mode 100644
index 0000000000..f9c8304bdc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/stress.py
@@ -0,0 +1,22 @@
+# META: timeout=long
+
+# Longer timeout required due to a bug in Chrome:
+# https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+
+import pytest
+
+from tests.support.asserts import assert_success
+from tests.support.helpers import document_hidden
+
+
+def minimize_window(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/minimize".format(**vars(session)))
+
+
+@pytest.mark.parametrize("i", range(5))
+def test_stress(session, i):
+ assert not document_hidden(session)
+ response = minimize_window(session)
+ assert_success(response)
+ assert document_hidden(session)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/minimize_window/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/user_prompts.py
new file mode 100644
index 0000000000..19059b3c39
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/minimize_window/user_prompts.py
@@ -0,0 +1,113 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+from tests.support.helpers import document_hidden
+
+
+def minimize(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/minimize".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ assert not document_hidden(session)
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = minimize(session)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+ assert document_hidden(session)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ assert not document_hidden(session)
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = minimize(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+ assert not document_hidden(session)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ assert not document_hidden(session)
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = minimize(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert not document_hidden(session)
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/navigate_to/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/navigate_to/file.py b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/file.py
new file mode 100644
index 0000000000..5dae5f5c4d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/file.py
@@ -0,0 +1,25 @@
+from tests.support import platform_name
+from tests.support.asserts import assert_success
+
+
+def navigate_to(session, url):
+ return session.transport.send(
+ "POST", "session/{session_id}/url".format(**vars(session)),
+ {"url": url})
+
+
+def test_file_protocol(session, server_config):
+ # tests that the browsing context remains the same
+ # when navigated privileged documents
+ path = server_config["doc_root"]
+ if platform_name == "windows":
+ # Convert the path into the format eg. /c:/foo/bar
+ path = "/{}".format(path.replace("\\", "/"))
+ url = u"file://{}".format(path)
+
+ response = navigate_to(session, url)
+ assert_success(response)
+
+ if session.url.endswith('/'):
+ url += '/'
+ assert session.url == url
diff --git a/testing/web-platform/tests/webdriver/tests/classic/navigate_to/navigate.py b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/navigate.py
new file mode 100644
index 0000000000..a9ff3f6a05
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/navigate.py
@@ -0,0 +1,93 @@
+import time
+
+import pytest
+from webdriver import error
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def navigate_to(session, url):
+ return session.transport.send(
+ "POST", "session/{session_id}/url".format(**vars(session)),
+ {"url": url})
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/url".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_null_response_value(session, inline):
+ response = navigate_to(session, inline("<div/>"))
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = navigate_to(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame, inline):
+ doc = inline("<p>foo")
+
+ response = navigate_to(session, doc)
+ assert_success(response)
+
+ assert session.url == doc
+
+
+@pytest.mark.parametrize("protocol,parameters", [
+ ("http", ""),
+ ("https", ""),
+ ("https", {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"})
+], ids=[
+ "http",
+ "https",
+ "https coop"
+])
+def test_seen_nodes(session, get_test_page, protocol, parameters):
+ first_page = get_test_page(parameters=parameters, protocol=protocol)
+ second_page = get_test_page(parameters=parameters, protocol=protocol, domain="alt")
+
+ response = navigate_to(session, first_page)
+ assert_success(response)
+
+ assert session.url == first_page
+
+ element = session.find.css("#custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ response = navigate_to(session, second_page)
+ assert_success(response)
+
+ assert session.url == second_page
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.name
+ with pytest.raises(error.DetachedShadowRootException):
+ shadow_root.find_element("css selector", "in-shadow-dom")
+
+ session.find.css("#custom-element", all=False)
+
+
+@pytest.mark.capabilities({"pageLoadStrategy": "eager"})
+def test_utf8_meta_tag_after_1024_bytes(session, url):
+ page = url("/webdriver/tests/support/html/meta-utf8-after-1024-bytes.html")
+
+ # Loading the page will cause a real parse commencing, and a renavigation
+ # to the same URL getting triggered subsequently. Test that the navigate
+ # command waits long enough.
+ response = navigate_to(session, page)
+ assert_success(response)
+
+ # If the command returns too early the property will be reset due to the
+ # subsequent page load.
+ session.execute_script("window.foo = 'bar'")
+
+ # Use delay to allow a possible missing subsequent navigation to start
+ time.sleep(1)
+
+ assert session.execute_script("return window.foo") == "bar"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/navigate_to/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/user_prompts.py
new file mode 100644
index 0000000000..db8d094ebb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/navigate_to/user_prompts.py
@@ -0,0 +1,185 @@
+# META: timeout=long
+
+import pytest
+from webdriver import error
+
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+
+
+def navigate_to(session, url):
+ return session.transport.send(
+ "POST", "session/{session_id}/url".format(**vars(session)),
+ {"url": url})
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+ page_target = url("/webdriver/tests/support/html/default.html")
+
+ response = navigate_to(session, page_beforeunload)
+ assert_success(response)
+
+ element = session.find.css("input", all=False)
+ element.send_keys("bar")
+
+ response = navigate_to(session, page_target)
+ assert_success(response)
+
+ assert session.url == page_target
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ url = inline("<div/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = navigate_to(session, url)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.url == url
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ url = inline("<div/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = navigate_to(session, url)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.url != url
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ url = inline("<div/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = navigate_to(session, url)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.url != url
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/conftest.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/conftest.py
new file mode 100644
index 0000000000..b6179aa90e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/conftest.py
@@ -0,0 +1,82 @@
+import pytest
+
+from webdriver.transport import HTTPWireProtocol
+
+
+def product(a, b):
+ return [(a, item) for item in b]
+
+
+def flatten(a):
+ return [item for x in a for item in x]
+
+
+@pytest.fixture(name="add_browser_capabilities")
+def fixture_add_browser_capabilities(configuration):
+ def add_browser_capabilities(capabilities):
+ # Make sure there aren't keys in common.
+ assert not set(configuration["capabilities"]).intersection(
+ set(capabilities))
+ result = dict(configuration["capabilities"])
+ result.update(capabilities)
+
+ return result
+
+ return add_browser_capabilities
+
+
+@pytest.fixture(name="configuration")
+def fixture_configuration(configuration):
+ """Remove "acceptInsecureCerts" from capabilities if it exists.
+
+ Some browser configurations add acceptInsecureCerts capability by default.
+ Remove it during new_session tests to avoid interference.
+ """
+
+ if "acceptInsecureCerts" in configuration["capabilities"]:
+ configuration = dict(configuration)
+ del configuration["capabilities"]["acceptInsecureCerts"]
+ return configuration
+
+
+@pytest.fixture(name="new_session")
+def fixture_new_session(request, configuration, current_session):
+ """Start a new session for tests which themselves test creating new sessions.
+
+ :param body: The content of the body for the new session POST request.
+
+ :param delete_existing_session: Allows the fixture to delete an already
+ created custom session before the new session is getting created. This
+ is useful for tests which call this fixture multiple times within the
+ same test.
+ """
+ custom_session = {}
+
+ transport = HTTPWireProtocol(
+ configuration["host"],
+ configuration["port"],
+ url_prefix="/",
+ )
+
+ def _delete_session(session_id):
+ transport.send("DELETE", "session/{}".format(session_id))
+
+ def new_session(body, delete_existing_session=False):
+ # If there is an active session from the global session fixture,
+ # delete that one first
+ if current_session is not None:
+ current_session.end()
+
+ if delete_existing_session:
+ _delete_session(custom_session["session"]["sessionId"])
+
+ response = transport.send("POST", "session", body)
+ if response.status == 200:
+ custom_session["session"] = response.body["value"]
+ return response, custom_session.get("session", None)
+
+ yield new_session
+
+ if custom_session.get("session") is not None:
+ _delete_session(custom_session["session"]["sessionId"])
+ custom_session = None
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/create_alwaysMatch.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/create_alwaysMatch.py
new file mode 100644
index 0000000000..64fd0a7425
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/create_alwaysMatch.py
@@ -0,0 +1,15 @@
+# META: timeout=long
+
+import pytest
+
+from .conftest import product, flatten
+
+from tests.support.asserts import assert_success
+from tests.classic.new_session.support.create import valid_data
+
+
+@pytest.mark.parametrize("key,value", flatten(product(*item) for item in valid_data))
+def test_valid(new_session, add_browser_capabilities, key, value):
+ response, _ = new_session({"capabilities": {
+ "alwaysMatch": add_browser_capabilities({key: value})}})
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/create_firstMatch.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/create_firstMatch.py
new file mode 100644
index 0000000000..d4523f4330
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/create_firstMatch.py
@@ -0,0 +1,16 @@
+# META: timeout=long
+
+import pytest
+
+from .conftest import product, flatten
+
+
+from tests.support.asserts import assert_success
+from tests.classic.new_session.support.create import valid_data
+
+
+@pytest.mark.parametrize("key,value", flatten(product(*item) for item in valid_data))
+def test_valid(new_session, add_browser_capabilities, key, value):
+ response, _ = new_session({"capabilities": {
+ "firstMatch": [add_browser_capabilities({key: value})]}})
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/default_values.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/default_values.py
new file mode 100644
index 0000000000..ac544c1338
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/default_values.py
@@ -0,0 +1,40 @@
+# META: timeout=long
+from tests.support.asserts import assert_error, assert_success
+
+
+def test_basic(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({})}})
+ value = assert_success(response)
+ assert set(value.keys()) == {"sessionId", "capabilities"}
+
+
+def test_repeat_new_session(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({})}})
+ assert_success(response)
+
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({})}})
+ assert_error(response, "session not created")
+
+
+def test_missing_first_match(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({})}})
+ assert_success(response)
+
+
+def test_missing_always_match(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {"firstMatch": [add_browser_capabilities({})]}})
+ assert_success(response)
+
+
+def test_desired(new_session, add_browser_capabilities):
+ response, _ = new_session({"desiredCapabilities": add_browser_capabilities({})})
+ assert_error(response, "invalid argument")
+
+
+def test_ignore_non_spec_fields_in_capabilities(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {
+ "alwaysMatch": add_browser_capabilities({}),
+ "desiredCapabilities": {"pageLoadStrategy": "eager"},
+ }})
+ value = assert_success(response)
+ assert value["capabilities"]["pageLoadStrategy"] == "normal"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/invalid_capabilities.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/invalid_capabilities.py
new file mode 100644
index 0000000000..be397edcf0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/invalid_capabilities.py
@@ -0,0 +1,56 @@
+import pytest
+
+from .conftest import product, flatten
+
+from tests.classic.new_session.support.create import invalid_data, invalid_extensions
+from tests.support.asserts import assert_error
+
+
+@pytest.mark.parametrize("value", [None, 1, "{}", []])
+def test_invalid_capabilites(new_session, value):
+ response, _ = new_session({"capabilities": value})
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, 1, "{}", []])
+def test_invalid_always_match(new_session, add_browser_capabilities, value):
+ capabilities = {"alwaysMatch": value, "firstMatch": [add_browser_capabilities({})]}
+
+ response, _ = new_session({"capabilities": capabilities})
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, 1, "[]", {}])
+def test_invalid_first_match(new_session, add_browser_capabilities, value):
+ capabilities = {"alwaysMatch": add_browser_capabilities({}), "firstMatch": value}
+
+ response, _ = new_session({"capabilities": capabilities})
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("body", [lambda key, value: {"alwaysMatch": {key: value}},
+ lambda key, value: {"firstMatch": [{key: value}]}])
+@pytest.mark.parametrize("key,value", flatten(product(*item) for item in invalid_data))
+def test_invalid_values(new_session, add_browser_capabilities, body, key, value):
+ capabilities = body(key, value)
+ if "alwaysMatch" in capabilities:
+ capabilities["alwaysMatch"] = add_browser_capabilities(capabilities["alwaysMatch"])
+ else:
+ capabilities["firstMatch"][0] = add_browser_capabilities(capabilities["firstMatch"][0])
+
+ response, _ = new_session({"capabilities": capabilities})
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("body", [lambda key, value: {"alwaysMatch": {key: value}},
+ lambda key, value: {"firstMatch": [{key: value}]}])
+@pytest.mark.parametrize("key", invalid_extensions)
+def test_invalid_extensions(new_session, add_browser_capabilities, body, key):
+ capabilities = body(key, {})
+ if "alwaysMatch" in capabilities:
+ capabilities["alwaysMatch"] = add_browser_capabilities(capabilities["alwaysMatch"])
+ else:
+ capabilities["firstMatch"][0] = add_browser_capabilities(capabilities["firstMatch"][0])
+
+ response, _ = new_session({"capabilities": capabilities})
+ assert_error(response, "invalid argument")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/merge.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/merge.py
new file mode 100644
index 0000000000..857d289fca
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/merge.py
@@ -0,0 +1,82 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support import platform_name
+
+
+@pytest.mark.skipif(platform_name is None, reason="Unsupported platform {}".format(platform_name))
+@pytest.mark.parametrize("body", [lambda key, value: {"alwaysMatch": {key: value}},
+ lambda key, value: {"firstMatch": [{key: value}]}])
+def test_platform_name(new_session, add_browser_capabilities, body):
+ capabilities = body("platformName", platform_name)
+ if "alwaysMatch" in capabilities:
+ capabilities["alwaysMatch"] = add_browser_capabilities(capabilities["alwaysMatch"])
+ else:
+ capabilities["firstMatch"][0] = add_browser_capabilities(capabilities["firstMatch"][0])
+
+ response, _ = new_session({"capabilities": capabilities})
+ value = assert_success(response)
+
+ assert value["capabilities"]["platformName"] == platform_name
+
+
+invalid_merge = [
+ ("acceptInsecureCerts", (True, True)),
+ ("unhandledPromptBehavior", ("accept", "accept")),
+ ("unhandledPromptBehavior", ("accept", "dismiss")),
+ ("timeouts", ({"script": 10}, {"script": 10})),
+ ("timeouts", ({"script": 10}, {"pageLoad": 10})),
+]
+
+
+@pytest.mark.parametrize("key,value", invalid_merge)
+def test_merge_invalid(new_session, add_browser_capabilities, key, value):
+ response, _ = new_session({"capabilities": {
+ "alwaysMatch": add_browser_capabilities({key: value[0]}),
+ "firstMatch": [{}, {key: value[1]}],
+ }})
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.skipif(platform_name is None, reason="Unsupported platform {}".format(platform_name))
+def test_merge_platformName(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {
+ "alwaysMatch": add_browser_capabilities({"timeouts": {"script": 10}}),
+ "firstMatch": [{
+ "platformName": platform_name.upper(),
+ "pageLoadStrategy": "none",
+ }, {
+ "platformName": platform_name,
+ "pageLoadStrategy": "eager",
+ }]}})
+
+ value = assert_success(response)
+
+ assert value["capabilities"]["platformName"] == platform_name
+ assert value["capabilities"]["pageLoadStrategy"] == "eager"
+
+
+def test_merge_browserName(new_session, add_browser_capabilities):
+ response, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({})}})
+ value = assert_success(response)
+
+ browser_settings = {
+ "browserName": value["capabilities"]["browserName"],
+ "browserVersion": value["capabilities"]["browserVersion"],
+ }
+
+ response, _ = new_session({"capabilities": {
+ "alwaysMatch": add_browser_capabilities({"timeouts": {"script": 10}}),
+ "firstMatch": [{
+ "browserName": browser_settings["browserName"] + "invalid",
+ "pageLoadStrategy": "none",
+ }, {
+ "browserName": browser_settings["browserName"],
+ "pageLoadStrategy": "eager",
+ }]}}, delete_existing_session=True)
+ value = assert_success(response)
+
+ assert value["capabilities"]["browserName"] == browser_settings['browserName']
+ assert value["capabilities"]["pageLoadStrategy"] == "eager"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/no_capabilities.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/no_capabilities.py
new file mode 100644
index 0000000000..31ee90555a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/no_capabilities.py
@@ -0,0 +1,8 @@
+from tests.support.asserts import assert_error
+
+# Passing no capabilities to the webdriver executable can cause various
+# side-effects. As such this particular test should be run separately.
+
+def test_no_capabilites(new_session):
+ response, _ = new_session({})
+ assert_error(response, "invalid argument")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/page_load_strategy.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/page_load_strategy.py
new file mode 100644
index 0000000000..69288ef433
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/page_load_strategy.py
@@ -0,0 +1,7 @@
+from tests.support.asserts import assert_success
+
+def test_pageLoadStrategy(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {
+ "alwaysMatch": add_browser_capabilities({"pageLoadStrategy": "eager"})}})
+ value = assert_success(response)
+ assert value["capabilities"]["pageLoadStrategy"] == "eager"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/platform_name.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/platform_name.py
new file mode 100644
index 0000000000..54fe4743be
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/platform_name.py
@@ -0,0 +1,11 @@
+import pytest
+
+from tests.support import platform_name
+from tests.support.asserts import assert_success
+
+
+@pytest.mark.skipif(platform_name is None, reason="Unsupported platform {}".format(platform_name))
+def test_corresponds_to_local_system(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({})}})
+ value = assert_success(response)
+ assert value["capabilities"]["platformName"] == platform_name
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/response.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/response.py
new file mode 100644
index 0000000000..43a8d57931
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/response.py
@@ -0,0 +1,44 @@
+import uuid
+import pytest
+
+from tests.support.asserts import assert_success
+
+
+def test_sessionid(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({})}})
+ value = assert_success(response)
+ assert isinstance(value["sessionId"], str)
+ uuid.UUID(hex=value["sessionId"])
+
+
+@pytest.mark.parametrize("capability, type", [
+ ("browserName", str),
+ ("browserVersion", str),
+ ("platformName", str),
+ ("acceptInsecureCerts", bool),
+ ("pageLoadStrategy", str),
+ ("proxy", dict),
+ ("setWindowRect", bool),
+ ("timeouts", dict),
+ ("strictFileInteractability", bool),
+ ("unhandledPromptBehavior", str),
+])
+def test_capability_type(session, capability, type):
+ assert isinstance(session.capabilities, dict)
+ assert capability in session.capabilities
+ assert isinstance(session.capabilities[capability], type)
+
+
+@pytest.mark.parametrize("capability, default_value", [
+ ("acceptInsecureCerts", False),
+ ("pageLoadStrategy", "normal"),
+ ("proxy", {}),
+ ("setWindowRect", True),
+ ("timeouts", {"implicit": 0, "pageLoad": 300000, "script": 30000}),
+ ("strictFileInteractability", False),
+ ("unhandledPromptBehavior", "dismiss and notify"),
+])
+def test_capability_default_value(session, capability, default_value):
+ assert isinstance(session.capabilities, dict)
+ assert capability in session.capabilities
+ assert session.capabilities[capability] == default_value
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/support/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/support/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/support/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/support/create.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/support/create.py
new file mode 100644
index 0000000000..a0d0ce37b5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/support/create.py
@@ -0,0 +1,136 @@
+# Note that we can only test things here all implementations must support
+valid_data = [
+ ("acceptInsecureCerts", [
+ False, None,
+ ]),
+ ("browserName", [
+ None,
+ ]),
+ ("browserVersion", [
+ None,
+ ]),
+ ("platformName", [
+ None,
+ ]),
+ ("pageLoadStrategy", [
+ None,
+ "none",
+ "eager",
+ "normal",
+ ]),
+ ("proxy", [
+ None,
+ ]),
+ ("timeouts", [
+ None, {},
+ {"script": 0, "pageLoad": 2.0, "implicit": 2**53 - 1},
+ {"script": 50, "pageLoad": 25},
+ {"script": 500},
+ ]),
+ ("strictFileInteractability", [
+ True, False, None,
+ ]),
+ ("unhandledPromptBehavior", [
+ "dismiss",
+ "accept",
+ None,
+ ]),
+ ("test:extension", [
+ None, False, "abc", 123, [],
+ {"key": "value"},
+ ]),
+]
+
+invalid_data = [
+ ("acceptInsecureCerts", [
+ 1, [], {}, "false",
+ ]),
+ ("browserName", [
+ 1, [], {}, False,
+ ]),
+ ("browserVersion", [
+ 1, [], {}, False,
+ ]),
+ ("platformName", [
+ 1, [], {}, False,
+ ]),
+ ("pageLoadStrategy", [
+ 1, [], {}, False,
+ "invalid",
+ "NONE",
+ "Eager",
+ "eagerblah",
+ "interactive",
+ " eager",
+ "eager "]),
+ ("proxy", [
+ 1, [], "{}",
+ {"proxyType": "SYSTEM"},
+ {"proxyType": "systemSomething"},
+ {"proxy type": "pac"},
+ {"proxy-Type": "system"},
+ {"proxy_type": "system"},
+ {"proxytype": "system"},
+ {"PROXYTYPE": "system"},
+ {"proxyType": None},
+ {"proxyType": 1},
+ {"proxyType": []},
+ {"proxyType": {"value": "system"}},
+ {" proxyType": "system"},
+ {"proxyType ": "system"},
+ {"proxyType ": " system"},
+ {"proxyType": "system "},
+ ]),
+ ("timeouts", [
+ 1, [], "{}", False,
+ {"invalid": 10},
+ {"PAGELOAD": 10},
+ {"page load": 10},
+ {" pageLoad": 10},
+ {"pageLoad ": 10},
+ {"pageLoad": None},
+ {"pageLoad": False},
+ {"pageLoad": []},
+ {"pageLoad": "10"},
+ {"pageLoad": 2.5},
+ {"pageLoad": -1},
+ {"pageLoad": 2**53},
+ {"pageLoad": {"value": 10}},
+ {"pageLoad": 10, "invalid": 10},
+ ]),
+ ("strictFileInteractability", [
+ 1, [], {}, "false",
+ ]),
+ ("unhandledPromptBehavior", [
+ 1, [], {}, False,
+ "DISMISS",
+ "dismissABC",
+ "Accept",
+ " dismiss",
+ "dismiss ",
+ ])
+]
+
+invalid_extensions = [
+ "automaticInspection",
+ "automaticProfiling",
+ "browser",
+ "chromeOptions",
+ "ensureCleanSession",
+ "firefox",
+ "firefox_binary",
+ "firefoxOptions",
+ "initialBrowserUrl",
+ "javascriptEnabled",
+ "logFile",
+ "logLevel",
+ "nativeEvents",
+ "platform",
+ "platformVersion",
+ "profile",
+ "requireWindowFocus",
+ "safari.options",
+ "seleniumProtocol",
+ "trustAllSSLCertificates",
+ "version",
+]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/timeouts.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/timeouts.py
new file mode 100644
index 0000000000..4f2652bba8
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/timeouts.py
@@ -0,0 +1,32 @@
+import pytest
+
+from tests.support.asserts import assert_success, assert_error
+
+
+def test_default_values(session):
+ timeouts = session.capabilities["timeouts"]
+
+ assert timeouts["implicit"] == 0
+ assert timeouts["pageLoad"] == 300000
+ assert timeouts["script"] == 30000
+
+
+@pytest.mark.parametrize("timeouts", [
+ {"implicit": 444, "pageLoad": 300000,"script": 30000},
+ {"implicit": 0, "pageLoad": 444,"script": 30000},
+ {"implicit": 0, "pageLoad": 300000,"script": 444},
+ {"implicit": 0, "pageLoad": 300000,"script": None},
+])
+def test_timeouts(new_session, add_browser_capabilities, timeouts):
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({"timeouts": timeouts})}})
+ value = assert_success(response)
+ assert value["capabilities"]["timeouts"] == timeouts
+
+@pytest.mark.parametrize("timeouts", [
+ {"implicit": None, "pageLoad": 300000,"script": 30000},
+ {"implicit": 0, "pageLoad": None,"script": 30000},
+ {"implicit": None, "pageLoad": None,"script": None}
+])
+def test_invalid_timeouts(new_session, add_browser_capabilities, timeouts):
+ response, _ = new_session({"capabilities": {"alwaysMatch": add_browser_capabilities({"timeouts": timeouts})}})
+ assert_error(response, "invalid argument")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_session/websocket_url.py b/testing/web-platform/tests/webdriver/tests/classic/new_session/websocket_url.py
new file mode 100644
index 0000000000..452decc90a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_session/websocket_url.py
@@ -0,0 +1,7 @@
+from tests.support.asserts import assert_success
+
+def test_websocket_url(new_session, add_browser_capabilities):
+ response, _ = new_session({"capabilities": {
+ "alwaysMatch": add_browser_capabilities({"webSocketUrl": True})}})
+ value = assert_success(response)
+ assert value["capabilities"]["webSocketUrl"].startswith("ws://")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_window/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/new_window/__init__.py
new file mode 100644
index 0000000000..e16014597c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_window/__init__.py
@@ -0,0 +1,10 @@
+def opener(session):
+ return session.execute_script("""
+ return window.opener;
+ """)
+
+
+def window_name(session):
+ return session.execute_script("""
+ return window.name;
+ """)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_window/new.py b/testing/web-platform/tests/webdriver/tests/classic/new_window/new.py
new file mode 100644
index 0000000000..fd0a1ffceb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_window/new.py
@@ -0,0 +1,64 @@
+import pytest
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def new_window(session, type_hint=None):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/new".format(**vars(session)),
+ {"type": type_hint})
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/window/new".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = new_window(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ original_handles = session.handles
+
+ response = new_window(session)
+ value = assert_success(response)
+ handles = session.handles
+ assert len(handles) == len(original_handles) + 1
+ assert value["handle"] in handles
+ assert value["handle"] not in original_handles
+ assert value["type"] in ["tab", "window"]
+
+
+@pytest.mark.parametrize("type_hint", [True, 42, 4.2, [], {}])
+def test_type_with_invalid_type(session, type_hint):
+ response = new_window(session, type_hint)
+ assert_error(response, "invalid argument")
+
+
+def test_type_with_null_value(session):
+ original_handles = session.handles
+
+ response = new_window(session, type_hint=None)
+ value = assert_success(response)
+ handles = session.handles
+ assert len(handles) == len(original_handles) + 1
+ assert value["handle"] in handles
+ assert value["handle"] not in original_handles
+ assert value["type"] in ["tab", "window"]
+
+
+def test_type_with_unknown_value(session):
+ original_handles = session.handles
+
+ response = new_window(session, type_hint="foo")
+ value = assert_success(response)
+ handles = session.handles
+ assert len(handles) == len(original_handles) + 1
+ assert value["handle"] in handles
+ assert value["handle"] not in original_handles
+ assert value["type"] in ["tab", "window"]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_window/new_tab.py b/testing/web-platform/tests/webdriver/tests/classic/new_window/new_tab.py
new file mode 100644
index 0000000000..f6cacf3c35
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_window/new_tab.py
@@ -0,0 +1,89 @@
+from tests.support.asserts import assert_success
+
+from . import opener, window_name
+
+
+def new_window(session, type_hint=None):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/new".format(**vars(session)),
+ {"type": type_hint})
+
+
+def test_payload(session):
+ original_handles = session.handles
+
+ response = new_window(session, type_hint="tab")
+ value = assert_success(response)
+ handles = session.handles
+ assert len(handles) == len(original_handles) + 1
+ assert value["handle"] in handles
+ assert value["handle"] not in original_handles
+ assert value["type"] == "tab"
+
+
+def test_keeps_current_window_handle(session):
+ original_handle = session.window_handle
+
+ response = new_window(session, type_hint="tab")
+ value = assert_success(response)
+ assert value["type"] == "tab"
+
+ assert session.window_handle == original_handle
+
+
+def test_opens_about_blank_in_new_tab(session, inline):
+ url = inline("<p>foo")
+ session.url = url
+
+ response = new_window(session, type_hint="tab")
+ value = assert_success(response)
+ assert value["type"] == "tab"
+
+ assert session.url == url
+
+ session.window_handle = value["handle"]
+ assert session.url == "about:blank"
+
+
+def test_sets_no_window_name(session):
+ response = new_window(session, type_hint="tab")
+ value = assert_success(response)
+ assert value["type"] == "tab"
+
+ session.window_handle = value["handle"]
+ assert window_name(session) == ""
+
+
+def test_sets_no_opener(session):
+ response = new_window(session, type_hint="tab")
+ value = assert_success(response)
+ assert value["type"] == "tab"
+
+ session.window_handle = value["handle"]
+ assert opener(session) is None
+
+
+def test_focus_content(session, inline):
+ response = new_window(session, type_hint="tab")
+ value = assert_success(response)
+ assert value["type"] == "tab"
+
+ session.window_handle = value["handle"]
+
+ session.url = inline("""
+ <span contenteditable="true"> abc </span>
+ <script>
+ const selection = getSelection();
+ window.onload = async() => {
+ const initial = document.querySelector("span");
+ initial.focus();
+ initial.setAttribute(
+ "_focused",
+ selection.anchorNode == initial.firstChild
+ );
+ }
+ </script>
+ """)
+
+ elem = session.find.css("span", all=False)
+ assert elem.attribute("_focused") == "true"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_window/new_window.py b/testing/web-platform/tests/webdriver/tests/classic/new_window/new_window.py
new file mode 100644
index 0000000000..d47dacdc08
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_window/new_window.py
@@ -0,0 +1,90 @@
+from tests.support.asserts import assert_success
+
+from . import opener, window_name
+
+
+def new_window(session, type_hint=None):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/new".format(**vars(session)),
+ {"type": type_hint})
+
+
+def test_payload(session):
+ original_handles = session.handles
+
+ response = new_window(session, type_hint="window")
+ value = assert_success(response)
+ handles = session.handles
+ assert len(handles) == len(original_handles) + 1
+ assert value["handle"] in handles
+ assert value["handle"] not in original_handles
+
+ # On Android applications have a single window only and a new tab will
+ # be opened instead.
+ if session.capabilities["platformName"] == "android":
+ assert value["type"] == "tab"
+ else:
+ assert value["type"] == "window"
+
+
+def test_keeps_current_window_handle(session):
+ original_handle = session.window_handle
+
+ response = new_window(session, type_hint="window")
+ value = assert_success(response)
+
+ assert session.window_handle == original_handle
+
+
+def test_opens_about_blank_in_new_window(session, inline):
+ url = inline("<p>foo")
+ session.url = url
+
+ response = new_window(session, type_hint="window")
+ value = assert_success(response)
+
+ assert session.url == url
+
+ session.window_handle = value["handle"]
+ assert session.url == "about:blank"
+
+
+def test_sets_no_window_name(session):
+ response = new_window(session, type_hint="window")
+ value = assert_success(response)
+
+ session.window_handle = value["handle"]
+ assert window_name(session) == ""
+
+
+def test_sets_no_opener(session):
+ response = new_window(session, type_hint="window")
+ value = assert_success(response)
+
+ session.window_handle = value["handle"]
+ assert opener(session) is None
+
+
+def test_focus_content(session, inline):
+ response = new_window(session, type_hint="window")
+ value = assert_success(response)
+
+ session.window_handle = value["handle"]
+
+ session.url = inline("""
+ <span contenteditable="true"> abc </span>
+ <script>
+ const selection = getSelection();
+ window.onload = async() => {
+ const initial = document.querySelector("span");
+ initial.focus();
+ initial.setAttribute(
+ "_focused",
+ selection.anchorNode == initial.firstChild
+ );
+ }
+ </script>
+ """)
+
+ elem = session.find.css("span", all=False)
+ assert elem.attribute("_focused") == "true"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/new_window/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/new_window/user_prompts.py
new file mode 100644
index 0000000000..0d841468ee
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/new_window/user_prompts.py
@@ -0,0 +1,121 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def new_window(session, type_hint=None):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/new".format(**vars(session)),
+ {"type": type_hint})
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ original_handles = session.handles
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = new_window(session)
+ value = assert_success(response)
+
+ handles = session.handles
+ assert len(handles) == len(original_handles) + 1
+ assert value["handle"] in handles
+ assert value["handle"] not in original_handles
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ original_handles = session.handles
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = new_window(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert len(session.handles) == len(original_handles)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ original_handles = session.handles
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = new_window(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert len(session.handles) == len(original_handles)
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/__init__.py
new file mode 100644
index 0000000000..af87e197d2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/__init__.py
@@ -0,0 +1,33 @@
+def assert_pointer_events(session, expected_events, target, pointer_type):
+ events = session.execute_script("return window.recordedEvents;")
+ assert len(events) == len(expected_events)
+ event_types = [e["type"] for e in events]
+ assert expected_events == event_types
+
+ for e in events:
+ assert e["target"] == target
+ assert e["pointerType"] == pointer_type
+
+
+def record_pointer_events(session, element):
+ # Record basic mouse / pointer events on a given element.
+ session.execute_script(
+ """
+ window.recordedEvents = [];
+ function onPointerEvent(event) {
+ window.recordedEvents.push({
+ "pointerType": event.pointerType,
+ "target": event.target.id,
+ "type": event.type,
+ });
+ }
+ arguments[0].addEventListener("pointerdown", onPointerEvent);
+ arguments[0].addEventListener("pointerup", onPointerEvent);
+ """,
+ args=(element,),
+ )
+def perform_actions(session, actions):
+ return session.transport.send(
+ "POST",
+ "/session/{session_id}/actions".format(session_id=session.session_id),
+ {"actions": actions})
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/conftest.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/conftest.py
new file mode 100644
index 0000000000..0694cce494
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/conftest.py
@@ -0,0 +1,89 @@
+import pytest
+
+from webdriver.error import NoSuchWindowException
+
+
+@pytest.fixture
+def session_new_window(capabilities, session):
+ # Prevent unreleased dragged elements by running the test in a new window.
+ original_handle = session.window_handle
+ session.window_handle = session.new_window()
+
+ yield session
+
+ try:
+ session.window.close()
+ except NoSuchWindowException:
+ pass
+
+ session.window_handle = original_handle
+
+
+@pytest.fixture
+def key_chain(session):
+ return session.actions.sequence("key", "keyboard_id")
+
+
+@pytest.fixture
+def mouse_chain(session):
+ return session.actions.sequence(
+ "pointer",
+ "pointer_id",
+ {"pointerType": "mouse"})
+
+
+@pytest.fixture
+def touch_chain(session):
+ return session.actions.sequence(
+ "pointer",
+ "pointer_id",
+ {"pointerType": "touch"})
+
+
+@pytest.fixture
+def pen_chain(session):
+ return session.actions.sequence(
+ "pointer",
+ "pointer_id",
+ {"pointerType": "pen"})
+
+
+@pytest.fixture
+def none_chain(session):
+ return session.actions.sequence("none", "none_id")
+
+
+@pytest.fixture
+def wheel_chain(session):
+ return session.actions.sequence("wheel", "wheel_id")
+
+
+@pytest.fixture(autouse=True)
+def release_actions(session, request):
+ # release all actions after each test
+ # equivalent to a teardown_function, but with access to session fixture
+ request.addfinalizer(session.actions.release)
+
+
+@pytest.fixture
+def key_reporter(session, test_actions_page, request):
+ """Represents focused input element from `test_actions_page` fixture."""
+ input_el = session.find.css("#keys", all=False)
+ input_el.click()
+ session.execute_script("resetEvents();")
+ return input_el
+
+
+@pytest.fixture
+def test_actions_page(session, url):
+ session.url = url("/webdriver/tests/support/html/test_actions.html")
+
+
+@pytest.fixture
+def test_actions_scroll_page(session, url):
+ session.url = url("/webdriver/tests/support/html/test_actions_scroll.html")
+
+
+@pytest.fixture
+def test_actions_pointer_page(session, url):
+ session.url = url("/webdriver/tests/support/html/test_actions_pointer.html")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/invalid.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/invalid.py
new file mode 100644
index 0000000000..f000477376
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/invalid.py
@@ -0,0 +1,842 @@
+import pytest
+
+from webdriver.error import InvalidArgumentException
+
+from tests.support.asserts import assert_error
+from . import perform_actions
+
+
+MAX_INT = 9007199254740991
+MIN_INT = -MAX_INT
+
+
+def create_pointer_common_object(pointer_action, overrides):
+ action = {
+ "type": pointer_action,
+ "width": 0,
+ "height": 0,
+ "pressure": 0.0,
+ "tangentialPressure": 0.0,
+ "twist": 0,
+ "tiltX": 0,
+ "tiltY": 0,
+ }
+
+ if pointer_action == "pointerMove":
+ action.update({"x": 0, "y": 0})
+ else:
+ action.update({"button": 0})
+
+ action.update(overrides)
+
+ return action
+
+
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+def test_input_source_action_sequence_invalid_type(session, value):
+ response = perform_actions(session, value)
+ assert_error(response, "invalid argument")
+
+
+def test_input_source_action_sequence_missing_type(session):
+ actions = [
+ {
+ "id": "foo",
+ "actions": [],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+def test_input_source_action_sequence_missing_id(session, action_type):
+ actions = [
+ {
+ "type": action_type,
+ "actions": [],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+def test_input_source_action_sequence_missing_actions(session, action_type):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+def test_input_source_action_sequence_type_invalid_type(session, value):
+ actions = [
+ {
+ "type": value,
+ "id": "foo",
+ "actions": [],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+def test_input_source_action_sequence_type_invalid_value(session):
+ for invalid_value in ["", "nones", "keys", "pointers", "wheels"]:
+ actions = [
+ {
+ "type": invalid_value,
+ "id": "foo",
+ "actions": [],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+def test_input_source_action_sequence_id_invalid_type(session, action_type, value):
+ actions = [
+ {
+ "type": action_type,
+ "id": value,
+ "actions": [],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+def test_input_source_action_sequence_actions_invalid_type(session, action_type, value):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": value,
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, "foo", True, 42, []])
+def test_input_source_action_sequence_pointer_parameters_invalid_type(session, value):
+ actions = [{"type": "pointer", "id": "foo", "actions": [], "parameters": value}]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+def test_input_source_action_sequence_pointer_parameters_pointer_type_invalid_type(
+ session, value
+):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [],
+ "parameters": {
+ "pointerType": value,
+ },
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", ["", "mouses", "pens", "touchs"])
+def test_input_source_action_sequence_pointer_parameters_pointer_type_invalid_value(
+ session, value
+):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [],
+ "parameters": {
+ "pointerType": value,
+ },
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+def test_input_source_action_sequence_actions_type_invalid_type(
+ session, action_type, value
+):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": [
+ {
+ "type": value,
+ "duration": 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", ["", "pauses"])
+def test_input_source_action_sequence_actions_subtype_invalid_value(
+ session, action_type, value
+):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": [
+ {
+ "type": value,
+ "duration": 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_input_source_action_sequence_actions_pause_duration_invalid_type(
+ session, action_type, value
+):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": [
+ {
+ "type": "pause",
+ "duration": value,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+def test_input_source_action_sequence_actions_pause_duration_invalid_value(
+ session, action_type, value
+):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": [{"type": "pause", "duration": value}],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", ["", "pauses"])
+def test_null_action_type_invalid_value(session, value):
+ actions = [
+ {
+ "type": "none",
+ "id": "foo",
+ "actions": [
+ {
+ "type": value,
+ "duration": 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", ["", "keyDowns", "keyUps"])
+def test_key_action_subtype_invalid_value(session, value):
+ actions = [
+ {
+ "type": "key",
+ "id": "foo",
+ "actions": [
+ {
+ "type": value,
+ "value": "f",
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("key_action", ["keyDown", "keyUp"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+def test_key_action_value_invalid_type(session, key_action, value):
+ actions = [
+ {
+ "type": "key",
+ "id": "foo",
+ "actions": [
+ {
+ "type": key_action,
+ "value": value,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", ["", "pointerDowns", "pointerMoves", "pointerUps"])
+def test_pointer_action_subtype_invalid_value(session, value):
+ if value == "pointerMoves":
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "pointerMoves",
+ "x": 0,
+ "y": 0,
+ }
+ ],
+ }
+ ]
+ else:
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [
+ {
+ "type": value,
+ "button": 0,
+ }
+ ],
+ }
+ ]
+
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_pointer_action_move_coordinate_invalid_type(session, coordinate, value):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "pointerMove",
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+def test_pointer_action_move_coordinate_invalid_value(session, coordinate, value):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "pointerMove",
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+def test_pointer_action_move_origin_invalid_type(session, value):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [{"type": "pointerMove", "x": 0, "y": 0, "origin": value}],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", ["", "pointers", "viewports"])
+def test_pointer_action_move_origin_invalid_value(session, value):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [{"type": "pointerMove", "x": 0, "y": 0, "origin": value}],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ {"frame-075b-4da1-b6ba-e579c2d3230a": "foo"},
+ {"shadow-6066-11e4-a52e-4f735466cecf": "foo"},
+ {"window-fcc6-11e5-b4f8-330a88ab9d7f": "foo"},
+ ],
+ ids=["frame", "shadow", "window"],
+)
+def test_pointer_action_move_origin_element_invalid_type(session, value):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [{"type": "pointerMove", "x": 0, "y": 0, "origin": value}],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+def test_pointer_action_move_origin_element_invalid_value(session):
+ value = {"element-6066-11e4-a52e-4f735466cecf": "foo"}
+
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [{"type": "pointerMove", "x": 0, "y": 0, "origin": value}],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+def test_pointer_action_up_down_button_missing(session, pointer_action):
+ actions = [
+ {
+ "type": "pointer",
+ "id": "foo",
+ "actions": [
+ {
+ "type": pointer_action,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_pointer_action_up_down_button_invalid_type(session, pointer_action, value):
+ action = create_pointer_common_object(pointer_action, {"button": value})
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+def test_pointer_action_up_down_button_invalid_value(session, pointer_action, value):
+ action = create_pointer_common_object(pointer_action, {"button": value})
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("dimension", ["width", "height"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_pointer_action_common_properties_dimensions_invalid_type(
+ session, dimension, pointer_action, value
+):
+ action = create_pointer_common_object(
+ pointer_action,
+ {
+ "width": value if dimension == "width" else 0,
+ "height": value if dimension == "height" else 0,
+ },
+ )
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("dimension", ["width", "height"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+def test_pointer_action_common_properties_dimensions_invalid_value(
+ session, dimension, pointer_action, value
+):
+ action = create_pointer_common_object(
+ pointer_action,
+ {
+ "width": value if dimension == "width" else 0,
+ "height": value if dimension == "height" else 0,
+ },
+ )
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("pressure", ["pressure", "tangentialPressure"])
+@pytest.mark.parametrize("value", [None, "foo", True, [], {}])
+def test_pointer_action_common_properties_pressure_invalid_type(
+ session, pointer_action, pressure, value
+):
+ action = create_pointer_common_object(
+ pointer_action,
+ {
+ "pressure": value if pressure == "pressure" else 0.0,
+ "tangentialPressure": value if pressure == "tangentialPressure" else 0.0,
+ },
+ )
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_pointer_action_common_properties_twist_invalid_type(
+ session, pointer_action, value
+):
+ action = create_pointer_common_object(pointer_action, {"twist": value})
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, 360])
+def test_pointer_action_common_properties_twist_invalid_value(
+ session, pointer_action, value
+):
+ action = create_pointer_common_object(pointer_action, {"twist": value})
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("angle", ["altitudeAngle", "azimuthAngle"])
+@pytest.mark.parametrize("value", [None, "foo", True, [], {}])
+def test_pointer_action_common_properties_angle_invalid_type(
+ session, pointer_action, angle, value
+):
+ action = create_pointer_common_object(
+ pointer_action,
+ {
+ "altitudeAngle": value if angle == "altitudeAngle" else 0.0,
+ "azimuthAngle": value if angle == "azimuthAngle" else 0.0,
+ },
+ )
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("tilt", ["tiltX", "tiltY"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_pointer_action_common_properties_tilt_invalid_type(
+ session, pointer_action, tilt, value
+):
+ action = create_pointer_common_object(
+ pointer_action,
+ {
+ "tiltX": value if tilt == "tiltX" else 0,
+ "tiltY": value if tilt == "tiltY" else 0,
+ },
+ )
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("tilt", ["tiltX", "tiltY"])
+@pytest.mark.parametrize("value", [-91, 91])
+def test_pointer_action_common_properties_tilt_invalid_value(
+ session, pointer_action, tilt, value
+):
+ action = create_pointer_common_object(
+ pointer_action,
+ {
+ "tiltX": value if tilt == "tiltX" else 0,
+ "tiltY": value if tilt == "tiltY" else 0,
+ },
+ )
+
+ response = perform_actions(
+ session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+ )
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_wheel_action_scroll_coordinate_invalid_type(session, coordinate, value):
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+def test_wheel_action_scroll_coordinate_invalid_value(session, coordinate, value):
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": value if coordinate == "x" else 0,
+ "y": value if coordinate == "y" else 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("delta", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+def test_wheel_action_scroll_delta_invalid_type(session, delta, value):
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": 0,
+ "y": 0,
+ "deltaX": value if delta == "x" else 0,
+ "deltaY": value if delta == "y" else 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("delta", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+def test_wheel_action_scroll_delta_invalid_value(session, delta, value):
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "deltaX": value if delta == "x" else 0,
+ "deltaY": value if delta == "y" else 0,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+def test_wheel_action_scroll_origin_invalid_type(session, value):
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": 0,
+ "y": 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ "origin": value,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", ["", "pointers", "viewports"])
+def test_wheel_action_scroll_origin_invalid_value(session, value):
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": 0,
+ "y": 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ "origin": value,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+def test_wheel_action_scroll_origin_pointer_not_supported(session):
+ # Pointer origin isn't currently supported for wheel input source
+ # See: https://github.com/w3c/webdriver/issues/1758
+
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": 0,
+ "y": 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ "origin": "pointer",
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ {"frame-075b-4da1-b6ba-e579c2d3230a": "foo"},
+ {"shadow-6066-11e4-a52e-4f735466cecf": "foo"},
+ {"window-fcc6-11e5-b4f8-330a88ab9d7f": "foo"},
+ ],
+ ids=["frame", "shadow", "window"],
+)
+def test_wheel_action_scroll_origin_element_invalid_type(session, value):
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": 0,
+ "y": 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ "origin": value,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "invalid argument")
+
+
+def test_wheel_action_scroll_origin_element_invalid_value(session):
+ value = {"element-6066-11e4-a52e-4f735466cecf": "foo"}
+
+ actions = [
+ {
+ "type": "wheel",
+ "id": "foo",
+ "actions": [
+ {
+ "type": "scroll",
+ "x": 0,
+ "y": 0,
+ "deltaX": 0,
+ "deltaY": 0,
+ "origin": value,
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("missing", ["x", "y", "deltaX", "deltaY"])
+def test_wheel_action_scroll_missing_property(
+ session, test_actions_scroll_page, wheel_chain, missing
+):
+ actions = wheel_chain.scroll(0, 0, 5, 10, origin="viewport")
+ del actions._actions[-1][missing]
+
+ with pytest.raises(InvalidArgumentException):
+ actions.perform()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key.py
new file mode 100644
index 0000000000..7809fcd01a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key.py
@@ -0,0 +1,62 @@
+import pytest
+
+from webdriver.error import NoSuchWindowException
+
+from tests.classic.perform_actions.support.refine import get_keys
+from tests.support.keys import Keys
+
+
+def test_null_response_value(session, key_chain):
+ value = key_chain.key_up("a").perform()
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window, key_chain):
+ with pytest.raises(NoSuchWindowException):
+ key_chain.key_up("a").perform()
+
+
+def test_no_browsing_context(session, closed_frame, key_chain):
+ with pytest.raises(NoSuchWindowException):
+ key_chain.key_up("a").perform()
+
+
+def test_element_not_focused(session, test_actions_page, key_chain):
+ key_reporter = session.find.css("#keys", all=False)
+
+ key_chain.key_down("a").key_up("a").perform()
+
+ assert get_keys(key_reporter) == ""
+
+
+def test_backspace_erases_keys(session, key_reporter, key_chain):
+ key_chain \
+ .send_keys("efcd") \
+ .send_keys([Keys.BACKSPACE, Keys.BACKSPACE]) \
+ .perform()
+
+ assert get_keys(key_reporter) == "ef"
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_element_in_shadow_tree(session, get_test_page, key_chain, mode, nested):
+ session.url = get_test_page(
+ shadow_doc="<div><input type=text></div>",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ )
+
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ if nested:
+ shadow_root = shadow_root.find_element(
+ "css selector", "inner-custom-element"
+ ).shadow_root
+
+ input_el = shadow_root.find_element("css selector", "input")
+ input_el.click()
+
+ key_chain.key_down("a").key_up("a").perform()
+
+ assert input_el.property("value") == "a"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_events.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_events.py
new file mode 100644
index 0000000000..a1cd9cea8e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_events.py
@@ -0,0 +1,223 @@
+# META: timeout=long
+import copy
+from collections import defaultdict
+
+import pytest
+
+from tests.classic.perform_actions.support.refine import get_events, get_keys
+from tests.support.helpers import filter_dict, filter_supported_key_events
+from tests.support.keys import ALL_EVENTS, ALTERNATIVE_KEY_NAMES, Keys
+
+
+def test_keyup_only_sends_no_events(session, key_reporter, key_chain):
+ key_chain.key_up("a").perform()
+
+ assert len(get_keys(key_reporter)) == 0
+ assert len(get_events(session)) == 0
+
+ session.actions.release()
+ assert len(get_keys(key_reporter)) == 0
+ assert len(get_events(session)) == 0
+
+
+@pytest.mark.parametrize("key, event", [
+ (Keys.ALT, "ALT"),
+ (Keys.CONTROL, "CONTROL"),
+ (Keys.META, "META"),
+ (Keys.SHIFT, "SHIFT"),
+ (Keys.R_ALT, "R_ALT"),
+ (Keys.R_CONTROL, "R_CONTROL"),
+ (Keys.R_META, "R_META"),
+ (Keys.R_SHIFT, "R_SHIFT"),
+])
+def test_modifier_key_sends_correct_events(session, key_reporter, key_chain, key, event):
+ code = ALL_EVENTS[event]["code"]
+ value = ALL_EVENTS[event]["key"]
+
+ if session.capabilities["browserName"] == "internet explorer":
+ key_reporter.click()
+ session.execute_script("resetEvents();")
+ key_chain \
+ .key_down(key) \
+ .key_up(key) \
+ .perform()
+ all_events = get_events(session)
+
+ expected = [
+ {"code": code, "key": value, "type": "keydown"},
+ {"code": code, "key": value, "type": "keyup"},
+ ]
+
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+ assert len(get_keys(key_reporter)) == 0
+
+
+@pytest.mark.parametrize("key,event", [
+ (Keys.ESCAPE, "ESCAPE"),
+ (Keys.RIGHT, "RIGHT"),
+])
+def test_non_printable_key_sends_events(session, key_reporter, key_chain, key, event):
+ code = ALL_EVENTS[event]["code"]
+ value = ALL_EVENTS[event]["key"]
+
+ key_chain \
+ .key_down(key) \
+ .key_up(key) \
+ .perform()
+ all_events = get_events(session)
+
+ expected = [
+ {"code": code, "key": value, "type": "keydown"},
+ {"code": code, "key": value, "type": "keypress"},
+ {"code": code, "key": value, "type": "keyup"},
+ ]
+
+ # Make a copy for alternate key property values
+ # Note: only keydown and keyup are affected by alternate key names
+ alt_expected = copy.deepcopy(expected)
+ if event in ALTERNATIVE_KEY_NAMES:
+ alt_expected[0]["key"] = ALTERNATIVE_KEY_NAMES[event]
+ alt_expected[2]["key"] = ALTERNATIVE_KEY_NAMES[event]
+
+ (_, expected) = filter_supported_key_events(all_events, expected)
+ (events, alt_expected) = filter_supported_key_events(all_events, alt_expected)
+ if len(events) == 2:
+ # most browsers don't send a keypress for non-printable keys
+ assert events == [expected[0], expected[2]] or events == [alt_expected[0], alt_expected[2]]
+ else:
+ assert events == expected or events == alt_expected
+
+ assert len(get_keys(key_reporter)) == 0
+
+
+@pytest.mark.parametrize("value,code", [
+ (u"a", "KeyA",),
+ ("a", "KeyA",),
+ (u"\"", "Quote"),
+ (u",", "Comma"),
+ (u"\u00E0", ""),
+ (u"\u0416", ""),
+ (u"@", "Digit2"),
+ (u"\u2603", ""),
+ (u"\uF6C2", ""), # PUA
+])
+def test_printable_key_sends_correct_events(session, key_reporter, key_chain, value, code):
+ key_chain \
+ .key_down(value) \
+ .key_up(value) \
+ .perform()
+ all_events = get_events(session)
+
+ expected = [
+ {"code": code, "key": value, "type": "keydown"},
+ {"code": code, "key": value, "type": "keypress"},
+ {"code": code, "key": value, "type": "keyup"},
+ ]
+
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+ assert get_keys(key_reporter) == value
+
+
+def test_sequence_of_keydown_printable_keys_sends_events(session, key_reporter, key_chain):
+ key_chain \
+ .key_down("a") \
+ .key_down("b") \
+ .perform()
+ all_events = get_events(session)
+
+ expected = [
+ {"code": "KeyA", "key": "a", "type": "keydown"},
+ {"code": "KeyA", "key": "a", "type": "keypress"},
+ {"code": "KeyB", "key": "b", "type": "keydown"},
+ {"code": "KeyB", "key": "b", "type": "keypress"},
+ ]
+
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+ assert get_keys(key_reporter) == "ab"
+
+
+def test_sequence_of_keydown_printable_characters_sends_events(session, key_reporter, key_chain):
+ key_chain.send_keys("ef").perform()
+ all_events = get_events(session)
+
+ expected = [
+ {"code": "KeyE", "key": "e", "type": "keydown"},
+ {"code": "KeyE", "key": "e", "type": "keypress"},
+ {"code": "KeyE", "key": "e", "type": "keyup"},
+ {"code": "KeyF", "key": "f", "type": "keydown"},
+ {"code": "KeyF", "key": "f", "type": "keypress"},
+ {"code": "KeyF", "key": "f", "type": "keyup"},
+ ]
+
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+ assert get_keys(key_reporter) == "ef"
+
+
+@pytest.mark.parametrize("name,expected", ALL_EVENTS.items())
+def test_special_key_sends_keydown(session, key_reporter, key_chain, name, expected):
+ if name.startswith("F"):
+ # Prevent default behavior for F1, etc., but only after keydown
+ # bubbles up to body. (Otherwise activated browser menus/functions
+ # may interfere with subsequent tests.)
+ session.execute_script("""
+ document.body.addEventListener("keydown",
+ function(e) { e.preventDefault() });
+ """)
+ if session.capabilities["browserName"] == "internet explorer":
+ key_reporter.click()
+ session.execute_script("resetEvents();")
+ key_chain.key_down(getattr(Keys, name)).perform()
+
+ # only interested in keydown
+ first_event = get_events(session)[0]
+ # make a copy so we can throw out irrelevant keys and compare to events
+ expected = dict(expected)
+
+ del expected["value"]
+
+ # make another copy for alternative key names
+ alt_expected = copy.deepcopy(expected)
+ if name in ALTERNATIVE_KEY_NAMES:
+ alt_expected["key"] = ALTERNATIVE_KEY_NAMES[name]
+
+ # check and remove keys that aren't in expected
+ assert first_event["type"] == "keydown"
+ assert first_event["repeat"] is False
+ first_event = filter_dict(first_event, expected)
+ if first_event["code"] is None:
+ del first_event["code"]
+ del expected["code"]
+ del alt_expected["code"]
+ assert first_event == expected or first_event == alt_expected
+ # only printable characters should be recorded in input field
+ entered_keys = get_keys(key_reporter)
+ if len(expected["key"]) == 1:
+ assert entered_keys == expected["key"]
+ else:
+ assert len(entered_keys) == 0
+
+
+def test_space_char_equals_pua(session, key_reporter, key_chain):
+ key_chain \
+ .key_down(Keys.SPACE) \
+ .key_up(Keys.SPACE) \
+ .key_down(" ") \
+ .key_up(" ") \
+ .perform()
+ all_events = get_events(session)
+ by_type = defaultdict(list)
+ for event in all_events:
+ by_type[event["type"]].append(event)
+
+ for event_type in by_type:
+ events = by_type[event_type]
+ assert len(events) == 2
+ assert events[0] == events[1]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_modifiers.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_modifiers.py
new file mode 100644
index 0000000000..652106f46c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_modifiers.py
@@ -0,0 +1,37 @@
+import pytest
+
+from tests.support.keys import Keys
+
+
+@pytest.mark.parametrize("modifier", [Keys.SHIFT, Keys.R_SHIFT])
+def test_shift_modifier_and_non_printable_keys(session, key_reporter, key_chain, modifier):
+ key_chain \
+ .send_keys("foo") \
+ .key_down(modifier) \
+ .key_down(Keys.BACKSPACE) \
+ .key_up(modifier) \
+ .key_up(Keys.BACKSPACE) \
+ .perform()
+
+ assert key_reporter.property("value") == "fo"
+
+
+@pytest.mark.parametrize("modifier", [Keys.SHIFT, Keys.R_SHIFT])
+def test_shift_modifier_generates_capital_letters(session, key_reporter, key_chain, modifier):
+ key_chain \
+ .send_keys("b") \
+ .key_down(modifier) \
+ .key_down("c") \
+ .key_up(modifier) \
+ .key_up("c") \
+ .key_down("d") \
+ .key_up("d") \
+ .key_down(modifier) \
+ .key_down("e") \
+ .key_up("e") \
+ .key_down("f") \
+ .key_up(modifier) \
+ .key_up("f") \
+ .perform()
+
+ assert key_reporter.property("value") == "bCdEF"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_shortcuts.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_shortcuts.py
new file mode 100644
index 0000000000..0e92e2f206
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_shortcuts.py
@@ -0,0 +1,47 @@
+from tests.classic.perform_actions.support.refine import get_keys
+from tests.support.keys import Keys
+
+
+def test_mod_a_and_backspace_deletes_all_text(session, key_reporter, key_chain, modifier_key):
+ key_chain.send_keys("abc d") \
+ .key_down(modifier_key) \
+ .key_down("a") \
+ .key_up(modifier_key) \
+ .key_up("a") \
+ .key_down(Keys.BACKSPACE) \
+ .perform()
+ assert get_keys(key_reporter) == ""
+
+
+def test_mod_a_mod_c_right_mod_v_pastes_text(session, key_reporter, key_chain, modifier_key):
+ initial = "abc d"
+ key_chain.send_keys(initial) \
+ .key_down(modifier_key) \
+ .key_down("a") \
+ .key_up(modifier_key) \
+ .key_up("a") \
+ .key_down(modifier_key) \
+ .key_down("c") \
+ .key_up(modifier_key) \
+ .key_up("c") \
+ .send_keys([Keys.RIGHT]) \
+ .key_down(modifier_key) \
+ .key_down("v") \
+ .key_up(modifier_key) \
+ .key_up("v") \
+ .perform()
+ assert get_keys(key_reporter) == initial * 2
+
+
+def test_mod_a_mod_x_deletes_all_text(session, key_reporter, key_chain, modifier_key):
+ key_chain.send_keys("abc d") \
+ .key_down(modifier_key) \
+ .key_down("a") \
+ .key_up(modifier_key) \
+ .key_up("a") \
+ .key_down(modifier_key) \
+ .key_down("x") \
+ .key_up(modifier_key) \
+ .key_up("x") \
+ .perform()
+ assert get_keys(key_reporter) == ""
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_special_keys.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_special_keys.py
new file mode 100644
index 0000000000..c55f3a113e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/key_special_keys.py
@@ -0,0 +1,38 @@
+import pytest
+
+from webdriver import error
+
+from tests.classic.perform_actions.support.refine import get_keys
+
+
+@pytest.mark.parametrize("value", [
+ (u"\U0001F604"),
+ (u"\U0001F60D"),
+ (u"\u0BA8\u0BBF"),
+ (u"\u1100\u1161\u11A8"),
+])
+def test_codepoint_keys_behave_correctly(session, key_reporter, key_chain, value):
+ # Not using key_chain.send_keys() because we always want to treat value as
+ # one character here. `len(value)` varies by platform for non-BMP characters,
+ # so we don't want to iterate over value.
+ key_chain \
+ .key_down(value) \
+ .key_up(value) \
+ .perform()
+
+ # events sent by major browsers are inconsistent so only check key value
+ assert get_keys(key_reporter) == value
+
+
+@pytest.mark.parametrize("value", [
+ (u"fa"),
+ (u"\u0BA8\u0BBFb"),
+ (u"\u0BA8\u0BBF\u0BA8"),
+ (u"\u1100\u1161\u11A8c")
+])
+def test_invalid_multiple_codepoint_keys_fail(session, key_reporter, key_chain, value):
+ with pytest.raises(error.InvalidArgumentException):
+ key_chain \
+ .key_down(value) \
+ .key_up(value) \
+ .perform()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/none.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/none.py
new file mode 100644
index 0000000000..b94a8f162d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/none.py
@@ -0,0 +1,17 @@
+from tests.support.asserts import assert_error, assert_success
+from . import perform_actions
+
+
+def test_null_response_value(session, none_chain):
+ response = perform_actions(session, [none_chain.pause(0).dict])
+ assert_success(response, None)
+
+
+def test_no_top_browsing_context(session, closed_window, none_chain):
+ response = perform_actions(session, [none_chain.pause(0).dict])
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame, none_chain):
+ response = perform_actions(session, [none_chain.pause(0).dict])
+ assert_error(response, "no such window")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/perform.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/perform.py
new file mode 100644
index 0000000000..3033394218
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/perform.py
@@ -0,0 +1,55 @@
+import pytest
+
+from tests.support.asserts import assert_success
+from . import perform_actions
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+def test_input_source_action_sequence_actions_pause_duration_valid(
+ session, action_type
+):
+ for valid_duration in [0, 1]:
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": [{"type": "pause", "duration": valid_duration}],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+def test_input_source_action_sequence_actions_pause_duration_missing(
+ session, action_type
+):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": [
+ {
+ "type": "pause",
+ }
+ ],
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_success(response)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "wheel"])
+def test_input_source_action_sequence_pointer_parameters_not_processed(
+ session, action_type
+):
+ actions = [
+ {
+ "type": action_type,
+ "id": "foo",
+ "actions": [],
+ "parameters": True,
+ }
+ ]
+ response = perform_actions(session, actions)
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_contextmenu.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_contextmenu.py
new file mode 100644
index 0000000000..4a48ea0b23
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_contextmenu.py
@@ -0,0 +1,78 @@
+import pytest
+
+from tests.classic.perform_actions.support.refine import get_events
+from tests.support.helpers import filter_dict
+from tests.support.keys import Keys
+
+
+@pytest.mark.parametrize("modifier, prop", [
+ (Keys.CONTROL, "ctrlKey"),
+ (Keys.R_CONTROL, "ctrlKey"),
+])
+def test_control_click(session, test_actions_page, key_chain, mouse_chain, modifier, prop):
+ os = session.capabilities["platformName"]
+ key_chain \
+ .pause(0) \
+ .key_down(modifier) \
+ .pause(200) \
+ .key_up(modifier)
+ outer = session.find.css("#outer", all=False)
+ mouse_chain.click(element=outer)
+ session.actions.perform([key_chain.dict, mouse_chain.dict])
+ if os != "mac":
+ expected = [
+ {"type": "mousemove"},
+ {"type": "mousedown"},
+ {"type": "mouseup"},
+ {"type": "click"},
+ ]
+ else:
+ expected = [
+ {"type": "mousemove"},
+ {"type": "mousedown"},
+ {"type": "contextmenu"},
+ {"type": "mouseup"},
+ ]
+ defaults = {
+ "altKey": False,
+ "metaKey": False,
+ "shiftKey": False,
+ "ctrlKey": False
+ }
+ for e in expected:
+ e.update(defaults)
+ if e["type"] != "mousemove":
+ e[prop] = True
+ filtered_events = [filter_dict(e, expected[0]) for e in get_events(session)]
+ assert expected == filtered_events
+
+
+def test_release_control_click(session, key_reporter, key_chain, mouse_chain):
+ # The context menu stays visible during subsequent tests so let's not
+ # display it in the first place.
+ session.execute_script("""
+ var keyReporter = document.getElementById("keys");
+ document.addEventListener("contextmenu", function(e) {
+ e.preventDefault();
+ });
+ """)
+ key_chain \
+ .pause(0) \
+ .key_down(Keys.CONTROL)
+ mouse_chain \
+ .pointer_move(0, 0, origin=key_reporter) \
+ .pointer_down()
+ session.actions.perform([key_chain.dict, mouse_chain.dict])
+ session.execute_script("""
+ var keyReporter = document.getElementById("keys");
+ keyReporter.addEventListener("mousedown", recordPointerEvent);
+ keyReporter.addEventListener("mouseup", recordPointerEvent);
+ resetEvents();
+ """)
+ session.actions.release()
+ expected = [
+ {"type": "mouseup"},
+ {"type": "keyup"},
+ ]
+ events = [filter_dict(e, expected[0]) for e in get_events(session)]
+ assert events == expected
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_dblclick.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_dblclick.py
new file mode 100644
index 0000000000..de83e77d36
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_dblclick.py
@@ -0,0 +1,58 @@
+import pytest
+
+from tests.classic.perform_actions.support.refine import get_events
+from tests.support.asserts import assert_move_to_coordinates
+from tests.support.helpers import filter_dict
+
+
+@pytest.mark.parametrize("click_pause", [0, 200])
+def test_dblclick_at_coordinates(session, test_actions_page, mouse_chain, click_pause):
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ mouse_chain \
+ .pointer_move(div_point["x"], div_point["y"]) \
+ .click() \
+ .pause(click_pause) \
+ .click() \
+ .perform()
+ events = get_events(session)
+ assert_move_to_coordinates(div_point, "outer", events)
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "dblclick", "button": 0},
+ ]
+ assert len(events) == 8
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
+
+
+def test_no_dblclick_when_mouse_moves(session, test_actions_page, mouse_chain):
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ mouse_chain \
+ .pointer_move(div_point["x"], div_point["y"]) \
+ .click() \
+ .pointer_move(div_point["x"] + 10, div_point["y"] + 10) \
+ .click() \
+ .perform()
+ events = get_events(session)
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ ]
+ assert len(events) == 7
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_modifier_click.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_modifier_click.py
new file mode 100644
index 0000000000..d1817e8fad
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_modifier_click.py
@@ -0,0 +1,91 @@
+import pytest
+
+from tests.classic.perform_actions.support.refine import get_events
+from tests.support.helpers import filter_dict
+from tests.support.keys import Keys
+
+
+@pytest.mark.parametrize("modifier, prop", [
+ (Keys.ALT, "altKey"),
+ (Keys.R_ALT, "altKey"),
+ (Keys.META, "metaKey"),
+ (Keys.R_META, "metaKey"),
+ (Keys.SHIFT, "shiftKey"),
+ (Keys.R_SHIFT, "shiftKey"),
+])
+def test_modifier_click(session, test_actions_page, key_chain, mouse_chain, modifier, prop):
+ key_chain \
+ .pause(200) \
+ .key_down(modifier) \
+ .pause(200) \
+ .pause(0) \
+ .key_up(modifier)
+ outer = session.find.css("#outer", all=False)
+ mouse_chain \
+ .pointer_move(0, 0, origin=outer) \
+ .pause(50) \
+ .pointer_down(0) \
+ .pointer_up(0) \
+ .pause(0)
+ session.actions.perform([key_chain.dict, mouse_chain.dict])
+ expected = [
+ {"type": "mousemove"},
+ {"type": "mousedown"},
+ {"type": "mouseup"},
+ {"type": "click"},
+ ]
+ defaults = {
+ "altKey": False,
+ "metaKey": False,
+ "shiftKey": False,
+ "ctrlKey": False
+ }
+ for e in expected:
+ e.update(defaults)
+ if e["type"] != "mousemove":
+ e[prop] = True
+ filtered_events = [filter_dict(e, expected[0]) for e in get_events(session)]
+ assert expected == filtered_events
+
+
+def test_many_modifiers_click(session, test_actions_page, key_chain, mouse_chain):
+ outer = session.find.css("#outer", all=False)
+ dblclick_timeout = 800
+ key_chain \
+ .pause(0) \
+ .key_down(Keys.ALT) \
+ .key_down(Keys.SHIFT) \
+ .pause(dblclick_timeout) \
+ .key_up(Keys.ALT) \
+ .key_up(Keys.SHIFT)
+ mouse_chain \
+ .pointer_move(0, 0, origin=outer) \
+ .pause(0) \
+ .pointer_down() \
+ .pointer_up() \
+ .pause(0) \
+ .pause(0) \
+ .pointer_down()
+ session.actions.perform([key_chain.dict, mouse_chain.dict])
+ expected = [
+ {"type": "mousemove"},
+ # shift and alt pressed
+ {"type": "mousedown"},
+ {"type": "mouseup"},
+ {"type": "click"},
+ # no modifiers pressed
+ {"type": "mousedown"},
+ ]
+ defaults = {
+ "altKey": False,
+ "metaKey": False,
+ "shiftKey": False,
+ "ctrlKey": False
+ }
+ for e in expected:
+ e.update(defaults)
+ for e in expected[1:4]:
+ e["shiftKey"] = True
+ e["altKey"] = True
+ events = [filter_dict(e, expected[0]) for e in get_events(session)]
+ assert events == expected
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse.py
new file mode 100644
index 0000000000..f8683ce451
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse.py
@@ -0,0 +1,289 @@
+import pytest
+
+from webdriver.error import InvalidArgumentException, NoSuchWindowException, StaleElementReferenceException
+
+from tests.classic.perform_actions.support.mouse import (
+ get_inview_center,
+ get_viewport_rect,
+)
+from tests.classic.perform_actions.support.refine import get_events
+from tests.support.asserts import assert_move_to_coordinates
+from tests.support.helpers import filter_dict
+from tests.support.sync import Poll
+
+from . import assert_pointer_events, record_pointer_events
+
+
+def test_null_response_value(session, mouse_chain):
+ value = mouse_chain.click().perform()
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window, mouse_chain):
+ with pytest.raises(NoSuchWindowException):
+ mouse_chain.click().perform()
+
+
+def test_no_browsing_context(session, closed_frame, mouse_chain):
+ with pytest.raises(NoSuchWindowException):
+ mouse_chain.click().perform()
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, mouse_chain, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ with pytest.raises(StaleElementReferenceException):
+ mouse_chain.click(element=element).perform()
+
+
+def test_click_at_coordinates(session, test_actions_page, mouse_chain):
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ mouse_chain \
+ .pointer_move(div_point["x"], div_point["y"], duration=1000) \
+ .click() \
+ .perform()
+ events = get_events(session)
+ assert len(events) == 4
+ assert_move_to_coordinates(div_point, "outer", events)
+ for e in events:
+ if e["type"] != "mousedown":
+ assert e["buttons"] == 0
+ assert e["button"] == 0
+ expected = [
+ {"type": "mousedown", "buttons": 1},
+ {"type": "mouseup", "buttons": 0},
+ {"type": "click", "buttons": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
+
+
+def test_context_menu_at_coordinates(session, test_actions_page, mouse_chain):
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ mouse_chain \
+ .pointer_move(div_point["x"], div_point["y"]) \
+ .pointer_down(button=2) \
+ .pointer_up(button=2) \
+ .perform()
+
+ events = get_events(session)
+ assert len(events) == 4
+
+ expected = [
+ {"type": "mousedown", "button": 2, "buttons": 2},
+ {"type": "contextmenu", "button": 2, "buttons": 2},
+ ]
+ # Some browsers in some platforms may dispatch `contextmenu` event as a
+ # a default action of `mouseup`. In the case, `.buttons` of the event
+ # should be 0.
+ anotherExpected = [
+ {"type": "mousedown", "button": 2, "buttons": 2},
+ {"type": "contextmenu", "button": 2, "buttons": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ mousedown_contextmenu_events = [
+ x for x in filtered_events
+ if x["type"] in ["mousedown", "contextmenu"]
+ ]
+ assert mousedown_contextmenu_events in [expected, anotherExpected]
+
+
+def test_middle_click(session, test_actions_page, mouse_chain):
+ div_point = {
+ "x": 82,
+ "y": 187,
+ }
+ mouse_chain \
+ .pointer_move(div_point["x"], div_point["y"]) \
+ .pointer_down(button=1) \
+ .pointer_up(button=1) \
+ .perform()
+
+ events = get_events(session)
+ assert len(events) == 3
+
+ expected = [
+ {"type": "mousedown", "button": 1, "buttons": 4},
+ {"type": "mouseup", "button": 1, "buttons": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ mousedown_mouseup_events = [
+ x for x in filtered_events
+ if x["type"] in ["mousedown", "mouseup"]
+ ]
+ assert expected == mousedown_mouseup_events
+
+
+def test_click_element_center(session, test_actions_page, mouse_chain):
+ outer = session.find.css("#outer", all=False)
+ center = get_inview_center(outer.rect, get_viewport_rect(session))
+ mouse_chain.click(element=outer).perform()
+ events = get_events(session)
+ assert len(events) == 4
+ event_types = [e["type"] for e in events]
+ assert ["mousemove", "mousedown", "mouseup", "click"] == event_types
+ for e in events:
+ if e["type"] != "mousemove":
+ assert e["pageX"] == pytest.approx(center["x"], abs=1.0)
+ assert e["pageY"] == pytest.approx(center["y"], abs=1.0)
+ assert e["target"] == "outer"
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_click_element_in_shadow_tree(
+ session, get_test_page, mouse_chain, mode, nested
+):
+ session.url = get_test_page(
+ shadow_doc="""
+ <div id="pointer-target"
+ style="width: 10px; height: 10px; background-color:blue;">
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ )
+
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+ if nested:
+ shadow_root = shadow_root.find_element("css selector", "inner-custom-element").shadow_root
+
+ target = shadow_root.find_element("css selector", "#pointer-target")
+ record_pointer_events(session, target)
+
+ mouse_chain.click(element=target).perform()
+ assert_pointer_events(
+ session,
+ expected_events=["pointerdown", "pointerup"],
+ target="pointer-target",
+ pointer_type="mouse",
+ )
+
+
+def test_click_navigation(session, url, inline):
+ destination = url("/webdriver/tests/support/html/test_actions.html")
+ start = inline("<a href=\"{}\" id=\"link\">destination</a>".format(destination))
+
+ def click(link):
+ mouse_chain = session.actions.sequence(
+ "pointer", "pointer_id", {"pointerType": "mouse"})
+ mouse_chain.click(element=link).perform()
+
+ session.url = start
+ error_message = "Did not navigate to %s" % destination
+
+ click(session.find.css("#link", all=False))
+ Poll(session, message=error_message).until(lambda s: s.url == destination)
+ # repeat steps to check behaviour after document unload
+ session.url = start
+ click(session.find.css("#link", all=False))
+ Poll(session, message=error_message).until(lambda s: s.url == destination)
+
+
+@pytest.mark.parametrize("x, y, event_count", [
+ (0, 0, 0),
+ (1, 0, 1),
+ (0, 1, 1),
+], ids=["default value", "x", "y"])
+def test_move_to_position_in_viewport(
+ session, test_actions_page, mouse_chain, x, y, event_count
+):
+ mouse_chain.pointer_move(x, y).perform()
+ events = get_events(session)
+ assert len(events) == event_count
+
+ # Move again to check that no further mouse move event is emitted.
+ mouse_chain.pointer_move(x, y).perform()
+ events = get_events(session)
+ assert len(events) == event_count
+
+
+@pytest.mark.parametrize("drag_duration", [0, 300, 800])
+@pytest.mark.parametrize("dx, dy", [
+ (20, 0), (0, 15), (10, 15), (-20, 0), (10, -15), (-10, -15)
+])
+def test_drag_and_drop(session,
+ test_actions_page,
+ mouse_chain,
+ dx,
+ dy,
+ drag_duration):
+ drag_target = session.find.css("#dragTarget", all=False)
+ initial_rect = drag_target.rect
+ initial_center = get_inview_center(initial_rect, get_viewport_rect(session))
+ # Conclude chain with extra move to allow time for last queued
+ # coordinate-update of drag_target and to test that drag_target is "dropped".
+ mouse_chain \
+ .pointer_move(0, 0, origin=drag_target) \
+ .pointer_down() \
+ .pointer_move(dx, dy, duration=drag_duration, origin="pointer") \
+ .pointer_up() \
+ .pointer_move(80, 50, duration=100, origin="pointer") \
+ .perform()
+ # mouseup that ends the drag is at the expected destination
+ e = get_events(session)[1]
+ assert e["type"] == "mouseup"
+ assert e["pageX"] == pytest.approx(initial_center["x"] + dx, abs=1.0)
+ assert e["pageY"] == pytest.approx(initial_center["y"] + dy, abs=1.0)
+ # check resulting location of the dragged element
+ final_rect = drag_target.rect
+ assert initial_rect["x"] + dx == final_rect["x"]
+ assert initial_rect["y"] + dy == final_rect["y"]
+
+
+@pytest.mark.parametrize("drag_duration", [0, 300, 800])
+def test_drag_and_drop_with_draggable_element(session_new_window,
+ test_actions_page,
+ mouse_chain,
+ drag_duration):
+ new_session = session_new_window
+ drag_target = new_session.find.css("#draggable", all=False)
+ drop_target = new_session.find.css("#droppable", all=False)
+ # Conclude chain with extra move to allow time for last queued
+ # coordinate-update of drag_target and to test that drag_target is "dropped".
+ mouse_chain \
+ .pointer_move(0, 0, origin=drag_target) \
+ .pointer_down() \
+ .pointer_move(50,
+ 25,
+ duration=drag_duration,
+ origin=drop_target) \
+ .pointer_up() \
+ .pointer_move(80, 50, duration=100, origin="pointer") \
+ .perform()
+ # mouseup that ends the drag is at the expected destination
+ e = get_events(new_session)
+ assert len(e) >= 5
+ assert e[1]["type"] == "dragstart", "Events captured were {}".format(e)
+ assert e[2]["type"] == "dragover", "Events captured were {}".format(e)
+ drag_events_captured = [
+ ev["type"] for ev in e if ev["type"].startswith("drag") or ev["type"].startswith("drop")
+ ]
+ assert "dragend" in drag_events_captured
+ assert "dragenter" in drag_events_captured
+ assert "dragleave" in drag_events_captured
+ assert "drop" in drag_events_captured
+
+
+@pytest.mark.parametrize("missing", ["x", "y"])
+def test_missing_coordinates(session, test_actions_page, mouse_chain, missing):
+ outer = session.find.css("#outer", all=False)
+ actions = mouse_chain.pointer_move(x=0, y=0, origin=outer)
+ del actions._actions[-1][missing]
+ with pytest.raises(InvalidArgumentException):
+ actions.perform()
+
+
+def test_invalid_element_origin(session, test_actions_page, mouse_chain):
+ outer = session.find.css("#outer", all=False)
+ actions = mouse_chain.pointer_move(
+ x=0, y=0, origin={"type": "element", "element": {"sharedId": outer.id}}
+ )
+ with pytest.raises(InvalidArgumentException):
+ actions.perform()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_origin.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_origin.py
new file mode 100644
index 0000000000..33b8a25959
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_origin.py
@@ -0,0 +1,123 @@
+import pytest
+
+from webdriver import MoveTargetOutOfBoundsException
+
+from tests.classic.perform_actions.support.mouse import (
+ get_inview_center,
+ get_viewport_rect,
+)
+
+
+def get_click_coordinates(session):
+ return session.execute_script("return window.coords;")
+
+
+def test_viewport_inside(session, mouse_chain, get_actions_origin_page):
+ point = {"x": 50, "y": 50}
+
+ session.url = get_actions_origin_page(
+ "width: 100px; height: 50px; background: green;"
+ )
+ mouse_chain.pointer_move(point["x"], point["y"], origin="viewport").perform()
+
+ click_coords = session.execute_script("return window.coords;")
+ assert click_coords["x"] == pytest.approx(point["x"], abs=1.0)
+ assert click_coords["y"] == pytest.approx(point["y"], abs=1.0)
+
+
+def test_viewport_outside(session, mouse_chain):
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ mouse_chain.pointer_move(-50, -50, origin="viewport").perform()
+
+
+def test_pointer_inside(session, mouse_chain, get_actions_origin_page):
+ start_point = {"x": 50, "y": 50}
+ offset = {"x": 10, "y": 5}
+
+ session.url = get_actions_origin_page(
+ "width: 100px; height: 50px; background: green;"
+ )
+ mouse_chain.pointer_move(start_point["x"], start_point["y"]).pointer_move(
+ offset["x"], offset["y"], origin="pointer"
+ ).perform()
+
+ click_coords = session.execute_script("return window.coords;")
+ assert click_coords["x"] == pytest.approx(start_point["x"] + offset["x"], abs=1.0)
+ assert click_coords["y"] == pytest.approx(start_point["y"] + offset["y"], abs=1.0)
+
+
+def test_pointer_outside(session, mouse_chain):
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ mouse_chain.pointer_move(-50, -50, origin="pointer").perform()
+
+
+def test_element_center_point(session, mouse_chain, get_actions_origin_page):
+ session.url = get_actions_origin_page(
+ "width: 100px; height: 50px; background: green;"
+ )
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain.pointer_move(0, 0, origin=elem).perform()
+
+ click_coords = get_click_coordinates(session)
+ assert click_coords["x"] == pytest.approx(center["x"], abs=1.0)
+ assert click_coords["y"] == pytest.approx(center["y"], abs=1.0)
+
+
+def test_element_center_point_with_offset(
+ session, mouse_chain, get_actions_origin_page
+):
+ session.url = get_actions_origin_page(
+ "width: 100px; height: 50px; background: green;"
+ )
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain.pointer_move(10, 15, origin=elem).perform()
+
+ click_coords = get_click_coordinates(session)
+ assert click_coords["x"] == pytest.approx(center["x"] + 10, abs=1.0)
+ assert click_coords["y"] == pytest.approx(center["y"] + 15, abs=1.0)
+
+
+def test_element_in_view_center_point_partly_visible(
+ session, mouse_chain, get_actions_origin_page
+):
+ session.url = get_actions_origin_page(
+ """width: 100px; height: 50px; background: green;
+ position: relative; left: -50px; top: -25px;"""
+ )
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain.pointer_move(0, 0, origin=elem).perform()
+
+ click_coords = get_click_coordinates(session)
+ assert click_coords["x"] == pytest.approx(center["x"], abs=1.0)
+ assert click_coords["y"] == pytest.approx(center["y"], abs=1.0)
+
+
+def test_element_larger_than_viewport(session, mouse_chain, get_actions_origin_page):
+ session.url = get_actions_origin_page(
+ "width: 300vw; height: 300vh; background: green;"
+ )
+ elem = session.find.css("#inner", all=False)
+ center = get_inview_center(elem.rect, get_viewport_rect(session))
+
+ mouse_chain.pointer_move(0, 0, origin=elem).perform()
+
+ click_coords = get_click_coordinates(session)
+ assert click_coords["x"] == pytest.approx(center["x"], abs=1.0)
+ assert click_coords["y"] == pytest.approx(center["y"], abs=1.0)
+
+
+def test_element_outside_of_view_port(session, mouse_chain, get_actions_origin_page):
+ session.url = get_actions_origin_page(
+ """width: 100px; height: 50px; background: green;
+ position: relative; left: -200px; top: -100px;"""
+ )
+ elem = session.find.css("#inner", all=False)
+
+ with pytest.raises(MoveTargetOutOfBoundsException):
+ mouse_chain.pointer_move(0, 0, origin=elem).perform()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pause_dblclick.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pause_dblclick.py
new file mode 100644
index 0000000000..fd14d08344
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pause_dblclick.py
@@ -0,0 +1,56 @@
+from tests.classic.perform_actions.support.mouse import (
+ get_inview_center,
+ get_viewport_rect,
+)
+from tests.classic.perform_actions.support.refine import get_events
+from tests.support.helpers import filter_dict
+
+_DBLCLICK_INTERVAL = 640
+
+
+def test_dblclick_with_pause_after_second_pointerdown(session, test_actions_page, mouse_chain):
+ outer = session.find.css("#outer", all=False)
+ center = get_inview_center(outer.rect, get_viewport_rect(session))
+ mouse_chain \
+ .pointer_move(int(center["x"]), int(center["y"])) \
+ .click() \
+ .pointer_down() \
+ .pause(_DBLCLICK_INTERVAL + 10) \
+ .pointer_up() \
+ .perform()
+ events = get_events(session)
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "dblclick", "button": 0},
+ ]
+ assert len(events) == 8
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
+
+
+def test_no_dblclick(session, test_actions_page, mouse_chain):
+ outer = session.find.css("#outer", all=False)
+ center = get_inview_center(outer.rect, get_viewport_rect(session))
+ mouse_chain \
+ .pointer_move(int(center["x"]), int(center["y"])) \
+ .click() \
+ .pause(_DBLCLICK_INTERVAL + 10) \
+ .click() \
+ .perform()
+ events = get_events(session)
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ ]
+ assert len(events) == 7
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pen.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pen.py
new file mode 100644
index 0000000000..bf71a20c4d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_pen.py
@@ -0,0 +1,113 @@
+import pytest
+
+from webdriver.error import NoSuchWindowException, StaleElementReferenceException
+
+from tests.classic.perform_actions.support.mouse import (
+ get_inview_center,
+ get_viewport_rect,
+)
+from tests.classic.perform_actions.support.refine import get_events
+
+from . import assert_pointer_events, record_pointer_events
+
+
+def test_null_response_value(session, pen_chain):
+ value = pen_chain.click().perform()
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window, pen_chain):
+ with pytest.raises(NoSuchWindowException):
+ pen_chain.click().perform()
+
+
+def test_no_browsing_context(session, closed_frame, pen_chain):
+ with pytest.raises(NoSuchWindowException):
+ pen_chain.click().perform()
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, pen_chain, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ with pytest.raises(StaleElementReferenceException):
+ pen_chain.click(element=element).perform()
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_pen_pointer_in_shadow_tree(
+ session, get_test_page, pen_chain, mode, nested
+):
+ session.url = get_test_page(
+ shadow_doc="""
+ <div id="pointer-target"
+ style="width: 10px; height: 10px; background-color:blue;">
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ )
+
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ if nested:
+ shadow_root = shadow_root.find_element(
+ "css selector", "inner-custom-element"
+ ).shadow_root
+
+ target = shadow_root.find_element("css selector", "#pointer-target")
+
+ record_pointer_events(session, target)
+
+ pen_chain.pointer_move(0, 0, origin=target) \
+ .pointer_down() \
+ .pointer_up() \
+ .perform()
+
+ assert_pointer_events(
+ session,
+ expected_events=["pointerdown", "pointerup"],
+ target="pointer-target",
+ pointer_type="pen",
+ )
+
+
+def test_pen_pointer_properties(session, test_actions_pointer_page, pen_chain):
+ pointerArea = session.find.css("#pointerArea", all=False)
+ center = get_inview_center(pointerArea.rect, get_viewport_rect(session))
+ pen_chain.pointer_move(0, 0, origin=pointerArea) \
+ .pointer_down(pressure=0.36, altitude_angle=0.3, azimuth_angle=0.2419, twist=86) \
+ .pointer_move(10, 10, origin=pointerArea) \
+ .pointer_up() \
+ .pointer_move(80, 50, origin=pointerArea) \
+ .perform()
+ events = get_events(session)
+ assert len(events) == 10
+ event_types = [e["type"] for e in events]
+ assert ["pointerover", "pointerenter", "pointermove", "pointerdown",
+ "pointerover", "pointerenter", "pointermove", "pointerup",
+ "pointerout", "pointerleave"] == event_types
+ assert events[3]["type"] == "pointerdown"
+ assert events[3]["pageX"] == pytest.approx(center["x"], abs=1.0)
+ assert events[3]["pageY"] == pytest.approx(center["y"], abs=1.0)
+ assert events[3]["target"] == "pointerArea"
+ assert events[3]["pointerType"] == "pen"
+ # The default value of width and height for mouse and pen inputs is 1
+ assert round(events[3]["width"], 2) == 1
+ assert round(events[3]["height"], 2) == 1
+ assert round(events[3]["pressure"], 2) == 0.36
+ assert events[3]["tiltX"] == 72
+ assert events[3]["tiltY"] == 38
+ assert events[3]["twist"] == 86
+ assert events[6]["type"] == "pointermove"
+ assert events[6]["pageX"] == pytest.approx(center["x"]+10, abs=1.0)
+ assert events[6]["pageY"] == pytest.approx(center["y"]+10, abs=1.0)
+ assert events[6]["target"] == "pointerArea"
+ assert events[6]["pointerType"] == "pen"
+ assert round(events[6]["width"], 2) == 1
+ assert round(events[6]["height"], 2) == 1
+ # The default value of pressure for all inputs is 0.5, other properties are 0
+ assert round(events[6]["pressure"], 2) == 0.5
+ assert events[6]["tiltX"] == 0
+ assert events[6]["tiltY"] == 0
+ assert events[6]["twist"] == 0
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_touch.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_touch.py
new file mode 100644
index 0000000000..b85b2e6ef3
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_touch.py
@@ -0,0 +1,145 @@
+import pytest
+
+from webdriver.error import NoSuchWindowException, StaleElementReferenceException
+from tests.classic.perform_actions.support.mouse import (
+ get_inview_center,
+ get_viewport_rect,
+)
+from tests.classic.perform_actions.support.refine import get_events
+
+from . import assert_pointer_events, record_pointer_events
+
+def test_null_response_value(session, touch_chain):
+ value = touch_chain.click().perform()
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window, touch_chain):
+ with pytest.raises(NoSuchWindowException):
+ touch_chain.click().perform()
+
+
+def test_no_browsing_context(session, closed_frame, touch_chain):
+ with pytest.raises(NoSuchWindowException):
+ touch_chain.click().perform()
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, touch_chain, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ with pytest.raises(StaleElementReferenceException):
+ touch_chain.click(element=element).perform()
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_touch_pointer_in_shadow_tree(
+ session, get_test_page, touch_chain, mode, nested
+):
+ session.url = get_test_page(
+ shadow_doc="""
+ <div id="pointer-target"
+ style="width: 10px; height: 10px; background-color:blue;">
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ )
+
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ if nested:
+ shadow_root = shadow_root.find_element(
+ "css selector", "inner-custom-element"
+ ).shadow_root
+
+ target = shadow_root.find_element("css selector", "#pointer-target")
+
+ record_pointer_events(session, target)
+
+ touch_chain.pointer_move(0, 0, origin=target).pointer_down().pointer_up().perform()
+
+ assert_pointer_events(
+ session,
+ expected_events=["pointerdown", "pointerup"],
+ target="pointer-target",
+ pointer_type="touch",
+ )
+
+
+def test_touch_pointer_properties(session, test_actions_pointer_page, touch_chain):
+ pointerArea = session.find.css("#pointerArea", all=False)
+ center = get_inview_center(pointerArea.rect, get_viewport_rect(session))
+ touch_chain.pointer_move(0, 0, origin=pointerArea) \
+ .pointer_down(width=23, height=31, pressure=0.78, twist=355) \
+ .pointer_move(10, 10, origin=pointerArea, width=39, height=35, pressure=0.91, twist=345) \
+ .pointer_up() \
+ .pointer_move(80, 50, origin=pointerArea) \
+ .perform()
+ events = get_events(session)
+ assert len(events) == 7
+ event_types = [e["type"] for e in events]
+ assert ["pointerover", "pointerenter", "pointerdown", "pointermove",
+ "pointerup", "pointerout", "pointerleave"] == event_types
+ assert events[2]["type"] == "pointerdown"
+ assert events[2]["pageX"] == pytest.approx(center["x"], abs=1.0)
+ assert events[2]["pageY"] == pytest.approx(center["y"], abs=1.0)
+ assert events[2]["target"] == "pointerArea"
+ assert events[2]["pointerType"] == "touch"
+ assert round(events[2]["width"], 2) == 23
+ assert round(events[2]["height"], 2) == 31
+ assert round(events[2]["pressure"], 2) == 0.78
+ assert events[3]["type"] == "pointermove"
+ assert events[3]["pageX"] == pytest.approx(center["x"]+10, abs=1.0)
+ assert events[3]["pageY"] == pytest.approx(center["y"]+10, abs=1.0)
+ assert events[3]["target"] == "pointerArea"
+ assert events[3]["pointerType"] == "touch"
+ assert round(events[3]["width"], 2) == 39
+ assert round(events[3]["height"], 2) == 35
+ assert round(events[3]["pressure"], 2) == 0.91
+
+
+def test_touch_pointer_properties_angle_twist(session, test_actions_pointer_page, touch_chain):
+ pointerArea = session.find.css("#pointerArea", all=False)
+ touch_chain.pointer_move(0, 0, origin=pointerArea) \
+ .pointer_down(width=23, height=31, pressure=0.78, altitude_angle=1.2, azimuth_angle=6, twist=355) \
+ .pointer_move(10, 10, origin=pointerArea, width=39, height=35, pressure=0.91, altitude_angle=0.5, azimuth_angle=1.8, twist=345) \
+ .pointer_up() \
+ .pointer_move(80, 50, origin=pointerArea) \
+ .perform()
+ events = get_events(session)
+ assert len(events) == 7
+ event_types = [e["type"] for e in events]
+ assert ["pointerover", "pointerenter", "pointerdown", "pointermove",
+ "pointerup", "pointerout", "pointerleave"] == event_types
+ assert events[2]["type"] == "pointerdown"
+ assert events[2]["tiltX"] == 20
+ assert events[2]["tiltY"] == -6
+ assert events[2]["twist"] == 355
+ assert events[3]["type"] == "pointermove"
+ assert events[3]["tiltX"] == -23
+ assert events[3]["tiltY"] == 61
+ assert events[3]["twist"] == 345
+
+
+def test_touch_pointer_properties_tilt_twist(session, test_actions_pointer_page, touch_chain):
+ pointerArea = session.find.css("#pointerArea", all=False)
+ touch_chain.pointer_move(0, 0, origin=pointerArea) \
+ .pointer_down(width=23, height=31, pressure=0.78, tilt_x=21, tilt_y=-8, twist=355) \
+ .pointer_move(10, 10, origin=pointerArea, width=39, height=35, pressure=0.91, tilt_x=-19, tilt_y=62, twist=345) \
+ .pointer_up() \
+ .pointer_move(80, 50, origin=pointerArea) \
+ .perform()
+ events = get_events(session)
+ assert len(events) == 7
+ event_types = [e["type"] for e in events]
+ assert ["pointerover", "pointerenter", "pointerdown", "pointermove",
+ "pointerup", "pointerout", "pointerleave"] == event_types
+ assert events[2]["type"] == "pointerdown"
+ assert events[2]["tiltX"] == 21
+ assert events[2]["tiltY"] == -8
+ assert events[2]["twist"] == 355
+ assert events[3]["type"] == "pointermove"
+ assert events[3]["tiltX"] == -19
+ assert events[3]["tiltY"] == 62
+ assert events[3]["twist"] == 345
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_tripleclick.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_tripleclick.py
new file mode 100644
index 0000000000..301b503ef9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_tripleclick.py
@@ -0,0 +1,30 @@
+import math
+
+lots_of_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor "\
+ "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud "\
+ "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
+
+
+def test_tripleclick_at_coordinates(session, mouse_chain, inline):
+ """
+ This test does a triple click on a coordinate. On desktop platforms
+ this will select a paragraph. On mobile this will not have the same
+ desired outcome as taps are handled differently on mobile.
+ """
+ session.url = inline("""<div>{}</div>""".format(lots_of_text))
+ div = session.find.css("div", all=False)
+ div_rect = div.rect
+ div_centre = {
+ "x": math.floor(div_rect["x"] + div_rect["width"] / 2),
+ "y": math.floor(div_rect["y"] + div_rect["height"] / 2)
+ }
+ mouse_chain \
+ .pointer_move(div_centre["x"], div_centre["y"]) \
+ .click() \
+ .click() \
+ .click() \
+ .perform()
+
+ actual_text = session.execute_script("return document.getSelection().toString();")
+
+ assert actual_text == lots_of_text
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/sequence.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/sequence.py
new file mode 100644
index 0000000000..7751762768
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/sequence.py
@@ -0,0 +1,7 @@
+from tests.classic.perform_actions.support.refine import get_events, get_keys
+
+
+def test_perform_no_actions_send_no_events(session, key_reporter, key_chain):
+ key_chain.perform()
+ assert len(get_keys(key_reporter)) == 0
+ assert len(get_events(session)) == 0
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/mouse.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/mouse.py
new file mode 100644
index 0000000000..b3672eb213
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/mouse.py
@@ -0,0 +1,26 @@
+def get_viewport_rect(session):
+ return session.execute_script("""
+ return {
+ height: window.innerHeight || document.documentElement.clientHeight,
+ width: window.innerWidth || document.documentElement.clientWidth,
+ };
+ """)
+
+
+def get_inview_center(elem_rect, viewport_rect):
+ x = {
+ "left": max(0, min(elem_rect["x"], elem_rect["x"] + elem_rect["width"])),
+ "right": min(viewport_rect["width"], max(elem_rect["x"],
+ elem_rect["x"] + elem_rect["width"])),
+ }
+
+ y = {
+ "top": max(0, min(elem_rect["y"], elem_rect["y"] + elem_rect["height"])),
+ "bottom": min(viewport_rect["height"], max(elem_rect["y"],
+ elem_rect["y"] + elem_rect["height"])),
+ }
+
+ return {
+ "x": (x["left"] + x["right"]) / 2,
+ "y": (y["top"] + y["bottom"]) / 2,
+ }
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/refine.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/refine.py
new file mode 100644
index 0000000000..a8fcb1f3b6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/support/refine.py
@@ -0,0 +1,29 @@
+def get_events(session):
+ """Return list of key events recorded in the test_keys_page fixture."""
+ events = session.execute_script("return allEvents.events;") or []
+ # `key` values in `allEvents` may be escaped (see `escapeSurrogateHalf` in
+ # test_keys_wdspec.html), so this converts them back into unicode literals.
+ for e in events:
+ # example: turn "U+d83d" (6 chars) into u"\ud83d" (1 char)
+ if "key" in e and e["key"].startswith(u"U+"):
+ key = e["key"]
+ hex_suffix = key[key.index("+") + 1:]
+ e["key"] = chr(int(hex_suffix, 16))
+
+ # WebKit sets code as 'Unidentified' for unidentified key codes, but
+ # tests expect ''.
+ if "code" in e and e["code"] == "Unidentified":
+ e["code"] = ""
+ return events
+
+
+def get_keys(input_el):
+ """Get printable characters entered into `input_el`.
+
+ :param input_el: HTML input element.
+ """
+ rv = input_el.property("value")
+ if rv is None:
+ return ""
+ else:
+ return rv
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/user_prompts.py
new file mode 100644
index 0000000000..7fcd720624
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/user_prompts.py
@@ -0,0 +1,144 @@
+# META: timeout=long
+
+import pytest
+from webdriver import error
+
+from tests.classic.perform_actions.support.refine import get_keys
+from tests.support.asserts import assert_error, assert_success, assert_dialog_handled
+from tests.support.sync import Poll
+from . import perform_actions
+
+actions = [{
+ "type": "key",
+ "id": "foobar",
+ "actions": [
+ {"type": "keyDown", "value": "a"},
+ {"type": "keyUp", "value": "a"},
+ ]
+}]
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, key_chain, key_reporter):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = perform_actions(session, actions)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert get_keys(key_reporter) == "a"
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, key_chain, key_reporter):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = perform_actions(session, actions)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert get_keys(key_reporter) == ""
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, key_reporter):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = perform_actions(session, actions)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert get_keys(key_reporter) == ""
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+def test_dismissed_beforeunload(session, url, mouse_chain):
+ page_beforeunload = url("/webdriver/tests/support/html/beforeunload.html")
+ page_target = url("/webdriver/tests/support/html/default.html")
+
+ session.url = page_beforeunload
+ input = session.find.css("input", all=False)
+ input.send_keys("bar")
+
+ link = session.find.css("a", all=False)
+
+ mouse_chain.pointer_move(0, 0, origin=link) \
+ .click() \
+ .perform()
+
+ wait = Poll(
+ session,
+ timeout=5,
+ message="Target page did not load")
+ wait.until(lambda s: s.url == page_target)
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py
new file mode 100644
index 0000000000..a75a84378a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/wheel.py
@@ -0,0 +1,113 @@
+import pytest
+
+from webdriver.error import NoSuchWindowException
+
+
+from tests.classic.perform_actions.support.refine import get_events
+
+
+def test_null_response_value(session, wheel_chain):
+ value = wheel_chain.scroll(0, 0, 0, 10).perform()
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window, wheel_chain):
+ with pytest.raises(NoSuchWindowException):
+ wheel_chain.scroll(0, 0, 0, 10).perform()
+
+
+def test_no_browsing_context(session, closed_window, wheel_chain):
+ with pytest.raises(NoSuchWindowException):
+ wheel_chain.scroll(0, 0, 0, 10).perform()
+
+
+def test_scroll_not_scrollable(session, test_actions_scroll_page, wheel_chain):
+ target = session.find.css("#not-scrollable", all=False)
+
+ wheel_chain.scroll(0, 0, 5, 10, origin=target).perform()
+
+ events = get_events(session)
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["deltaX"] == 5
+ assert events[0]["deltaY"] == 10
+ assert events[0]["deltaZ"] == 0
+ assert events[0]["target"] == "not-scrollable-content"
+
+
+def test_scroll_scrollable_overflow(session, test_actions_scroll_page, wheel_chain):
+ target = session.find.css("#scrollable", all=False)
+
+ wheel_chain.scroll(0, 0, 5, 10, origin=target).perform()
+
+ events = get_events(session)
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["deltaX"] == 5
+ assert events[0]["deltaY"] == 10
+ assert events[0]["deltaZ"] == 0
+ assert events[0]["target"] == "scrollable-content"
+
+
+def test_scroll_iframe(session, test_actions_scroll_page, wheel_chain):
+ target = session.find.css("#iframe", all=False)
+
+ wheel_chain.scroll(0, 0, 5, 10, origin=target).perform()
+
+ events = get_events(session)
+ assert len(events) == 1
+ assert events[0]["type"] == "wheel"
+ assert events[0]["deltaX"] == 5
+ assert events[0]["deltaY"] == 10
+ assert events[0]["deltaZ"] == 0
+ assert events[0]["target"] == "iframeContent"
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_scroll_shadow_tree(session, get_test_page, wheel_chain, mode, nested):
+ session.url = get_test_page(
+ shadow_doc="""
+ <div id="scrollableShadowTree"
+ style="width: 100px; height: 100px; overflow: auto;">
+ <div
+ id="scrollableShadowTreeContent"
+ style="width: 600px; height: 1000px; background-color:blue"></div>
+ </div>""",
+ shadow_root_mode=mode,
+ nested_shadow_dom=nested,
+ )
+
+ shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+ if nested:
+ shadow_root = shadow_root.find_element(
+ "css selector", "inner-custom-element"
+ ).shadow_root
+
+ scrollable = shadow_root.find_element("css selector", "#scrollableShadowTree")
+
+ # Add a simplified event recorder to track events in the test ShadowRoot.
+ session.execute_script(
+ """
+ window.wheelEvents = [];
+ arguments[0].addEventListener("wheel",
+ function(event) {
+ window.wheelEvents.push({
+ "deltaX": event.deltaX,
+ "deltaY": event.deltaY,
+ "target": event.target.id
+ });
+ }
+ );
+ """,
+ args=(scrollable,),
+ )
+
+ wheel_chain.scroll(0, 0, 5, 10, origin=scrollable).perform()
+
+ events = session.execute_script("return window.wheelEvents;") or []
+ assert len(events) == 1
+ assert events[0]["deltaX"] == 5
+ assert events[0]["deltaY"] == 10
+ assert events[0]["target"] == "scrollableShadowTreeContent"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/permissions/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/permissions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/permissions/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/permissions/set.py b/testing/web-platform/tests/webdriver/tests/classic/permissions/set.py
new file mode 100644
index 0000000000..740b93d40e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/permissions/set.py
@@ -0,0 +1,119 @@
+from tests.support.asserts import assert_error, assert_success
+import pytest
+
+
+def query(session, name):
+ script = """
+ var done = arguments[0];
+ navigator.permissions.query({ name: '%s' })
+ .then(function(value) {
+ done({ status: 'success', value: value && value.state });
+ }, function(error) {
+ done({ status: 'error', value: error && error.message });
+ });
+ """ % name
+
+ return session.transport.send(
+ "POST", "/session/{session_id}/execute/async".format(**vars(session)),
+ {
+ "script": script,
+ "args": []
+ })
+
+
+# > 1. Let parameters be the parameters argument, converted to an IDL value of
+# > type PermissionSetParameters. If this throws an exception, return a
+# > WebDriver error with WebDriver error code invalid argument.
+@pytest.mark.parametrize(
+ "parameters",
+ [
+ #{ "descriptor": { "name": "geolocation" }, "state": "granted" }
+ {
+ "descriptor": {
+ "name": 23
+ },
+ "state": "granted"
+ },
+ {
+ "descriptor": {},
+ "state": "granted"
+ },
+ {
+ "descriptor": {
+ "name": "geolocation"
+ },
+ "state": "Granted"
+ },
+ {
+ "descriptor": 23,
+ "state": "granted"
+ },
+ {
+ "descriptor": "geolocation",
+ "state": "granted"
+ },
+ {
+ "descriptor": [{
+ "name": "geolocation"
+ }],
+ "state": "granted"
+ },
+ [{
+ "descriptor": {
+ "name": "geolocation"
+ },
+ "state": "granted"
+ }],
+ ])
+def test_invalid_parameters(session, url, parameters):
+ session.url = url("/common/blank.html", protocol="https")
+ response = session.transport.send(
+ "POST", "/session/{session_id}/permissions".format(**vars(session)),
+ parameters)
+ assert_error(response, "invalid argument")
+
+
+# > 6. If settings is a non-secure context and rootDesc.name isn't allowed in
+# > non-secure contexts, return a WebDriver error with WebDriver error code
+# > invalid argument.
+@pytest.mark.parametrize("state", ["granted", "denied", "prompt"])
+def test_non_secure_context(session, url, state):
+ session.url = url("/common/blank.html", protocol="http")
+ response = session.transport.send(
+ "POST", "/session/{session_id}/permissions".format(**vars(session)), {
+ "descriptor": {
+ "name": "push"
+ },
+ "state": state
+ })
+
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("state", ["granted", "denied", "prompt"])
+def test_set_to_state(session, url, state):
+ session.url = url("/common/blank.html", protocol="https")
+ parameters = {"descriptor": {"name": "geolocation"}, "state": state}
+ response = session.transport.send(
+ "POST", "/session/{session_id}/permissions".format(**vars(session)),
+ parameters)
+
+ try:
+ assert_success(response)
+ except AssertionError:
+ # > 4. If parameters.state is an inappropriate permission state for any
+ # > implementation-defined reason, return a WebDriver error with
+ # > WebDriver error code invalid argument.
+ assert_error(response, "invalid argument")
+ return
+
+ assert response.body.get("value") is None
+
+ response = query(session, "geolocation")
+
+ assert_success(response)
+ result = response.body.get("value")
+
+ assert isinstance(result, dict)
+ assert result.get("status") == "success"
+ assert result.get("value") == state
diff --git a/testing/web-platform/tests/webdriver/tests/classic/print/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/print/__init__.py
new file mode 100644
index 0000000000..eb9a890cc4
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/print/__init__.py
@@ -0,0 +1,21 @@
+def do_print(session, options={}):
+ params = {}
+
+ if options.get("background", None) is not None:
+ params["background"] = options["background"]
+ if options.get("margin", None) is not None:
+ params["margin"] = options["margin"]
+ if options.get("orientation") is not None:
+ params["orientation"] = options["orientation"]
+ if options.get("page") is not None:
+ params["page"] = options["page"]
+ if options.get("pageRanges") is not None:
+ params["pageRanges"] = options["pageRanges"]
+ if options.get("scale") is not None:
+ params["scale"] = options["scale"]
+ if options.get("shrinkToFit") is not None:
+ params["shrinkToFit"] = options["shrinkToFit"]
+
+ return session.transport.send(
+ "POST", "session/{session_id}/print".format(**vars(session)), params
+ )
diff --git a/testing/web-platform/tests/webdriver/tests/classic/print/background.py b/testing/web-platform/tests/webdriver/tests/classic/print/background.py
new file mode 100644
index 0000000000..4f2f85980b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/print/background.py
@@ -0,0 +1,58 @@
+import base64
+
+import pytest
+
+from tests.support.asserts import assert_pdf, assert_success
+from tests.support.image import px_to_cm
+
+from . import do_print
+
+
+INLINE_BACKGROUND_RENDERING_TEST_CONTENT = """
+<style>
+:root {
+ background-color: black;
+}
+</style>
+"""
+
+BLACK_DOT_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYGD4DwABBAEAwS2OUAAAAABJRU5ErkJggg=="
+WHITE_DOT_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2P4DwQACfsD/Z8fLAAAAAAASUVORK5CYII="
+
+
+@pytest.mark.parametrize(
+ "print_with_background, expected_image",
+ [
+ (None, WHITE_DOT_PNG),
+ (True, BLACK_DOT_PNG),
+ (False, WHITE_DOT_PNG),
+ ],
+)
+def test_background(
+ session,
+ inline,
+ compare_png_http,
+ render_pdf_to_png_http,
+ print_with_background,
+ expected_image,
+):
+ session.url = inline(INLINE_BACKGROUND_RENDERING_TEST_CONTENT)
+
+ print_result = do_print(
+ session,
+ {
+ "background": print_with_background,
+ "margin": {"top": 0, "bottom": 0, "right": 0, "left": 0},
+ "page": {"width": px_to_cm(1), "height": px_to_cm(1)},
+ },
+ )
+ print_value = assert_success(print_result)
+ assert_pdf(print_value)
+
+ png = render_pdf_to_png_http(
+ print_value
+ )
+ comparison = compare_png_http(
+ png, base64.b64decode(expected_image)
+ )
+ assert comparison.equal()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/print/orientation.py b/testing/web-platform/tests/webdriver/tests/classic/print/orientation.py
new file mode 100644
index 0000000000..107cf380df
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/print/orientation.py
@@ -0,0 +1,43 @@
+import pytest
+
+from tests.support.asserts import assert_pdf, assert_success
+from tests.support.image import png_dimensions
+
+from . import do_print
+
+
+@pytest.mark.parametrize(
+ "orientation_value, is_portrait",
+ [
+ (None, True),
+ ("portrait", True),
+ ("landscape", False),
+ ],
+ ids=[
+ "default",
+ "portrait",
+ "landscape",
+ ],
+)
+def test_orientation(
+ session,
+ inline,
+ render_pdf_to_png_http,
+ orientation_value,
+ is_portrait,
+):
+ session.url = inline("")
+
+ print_result = do_print(
+ session,
+ {
+ "orientation": orientation_value
+ },
+ )
+ print_value = assert_success(print_result)
+ assert_pdf(print_value)
+
+ png = render_pdf_to_png_http(print_value)
+ width, height = png_dimensions(png)
+
+ assert (width < height) == is_portrait
diff --git a/testing/web-platform/tests/webdriver/tests/classic/print/printcmd.py b/testing/web-platform/tests/webdriver/tests/classic/print/printcmd.py
new file mode 100644
index 0000000000..07d419d1bb
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/print/printcmd.py
@@ -0,0 +1,131 @@
+# META: timeout=long
+from base64 import decodebytes
+
+import pytest
+
+from tests.support.asserts import assert_error, assert_pdf, assert_success
+
+from . import do_print
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = do_print(session, {})
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = do_print(session, {})
+ value = assert_success(response)
+ assert_pdf(value)
+
+
+def test_html_document(session, inline):
+ session.url = inline("Test")
+
+ response = do_print(session, {
+ "page": {"width": 10,
+ "height": 20},
+ "shrinkToFit": False
+ })
+ value = assert_success(response)
+ # TODO: Test that the output is reasonable
+ assert_pdf(value)
+
+
+def test_large_html_document(session, inline):
+ session.url = inline("<canvas id=\"image\"></canvas>")
+
+ session.execute_script(
+ """
+ const width = 700;
+ const height = 900;
+
+ const canvas = document.getElementById("image");
+ const context = canvas.getContext("2d");
+
+ canvas.width = width;
+ canvas.height = height;
+
+ for (let x = 0; x < width; ++x) {
+ for (let y = 0; y < height; ++y) {
+ const colourHex = Math.floor(Math.random() * 0xffffff).toString(16);
+
+ context.fillStyle = `#${colourHex}`;
+ context.fillRect(x, y, 1, 1);
+ }
+ }
+ """
+ )
+
+ response = do_print(session, {})
+ value = assert_success(response)
+ pdf = decodebytes(value.encode())
+
+ # This was added to test the fix for a bug in firefox where a PDF larger
+ # than 500kb would cause an error. If the resulting PDF is smaller than that
+ # it could pass incorrectly.
+ assert len(pdf) > 500000
+ assert_pdf(value)
+
+
+@pytest.mark.parametrize("ranges,expected", [
+ (["2-4"], ["Page 2", "Page 3", "Page 4"]),
+ (["2-4", "2-3"], ["Page 2", "Page 3", "Page 4"]),
+ (["2-4", "3-5"], ["Page 2", "Page 3", "Page 4", "Page 5"]),
+ (["9-"], ["Page 9", "Page 10"]),
+ (["-2"], ["Page 1", "Page 2"]),
+ (["7"], ["Page 7"]),
+ ([7],["Page 7"]),
+ (["-2", "9-", "7"], ["Page 1", "Page 2", "Page 7", "Page 9", "Page 10"]),
+ (["-2", "9-", 7], ["Page 1", "Page 2", "Page 7", "Page 9", "Page 10"]),
+ (["-5", "2-"], ["Page 1", "Page 2", "Page 3", "Page 4", "Page 5", "Page 6", "Page 7", "Page 8", "Page 9", "Page 10"]),
+ ([], ["Page 1", "Page 2", "Page 3", "Page 4", "Page 5", "Page 6", "Page 7", "Page 8", "Page 9", "Page 10"]),
+])
+def test_page_ranges_document(session, inline, load_pdf_http, ranges, expected):
+ session.url = inline("""
+<style>
+div {page-break-after: always}
+</style>
+
+<div>Page 1</div>
+<div>Page 2</div>
+<div>Page 3</div>
+<div>Page 4</div>
+<div>Page 5</div>
+<div>Page 6</div>
+<div>Page 7</div>
+<div>Page 8</div>
+<div>Page 9</div>
+<div>Page 10</div>""")
+
+ response = do_print(session, {
+ "pageRanges": ranges
+ })
+ value = assert_success(response)
+ # TODO: Test that the output is reasonable
+ assert_pdf(value)
+
+ load_pdf_http(value)
+ pages = session.execute_async_script("""let callback = arguments[arguments.length - 1];
+window.getText().then(pages => callback(pages));""")
+ assert pages == expected
+
+
+@pytest.mark.parametrize("options", [{"orientation": 0},
+ {"orientation": "foo"},
+ {"scale": "1"},
+ {"scale": 3},
+ {"scale": 0.01},
+ {"margin": {"top": "1"}},
+ {"margin": {"bottom": -1}},
+ {"page": {"height": False}},
+ {"shrinkToFit": "false"},
+ {"pageRanges": ["3-2"]},
+ {"pageRanges": ["a-2"]},
+ {"pageRanges": ["1:2"]},
+ {"pageRanges": ["1-2-3"]},
+ {"pageRanges": [None]},
+ {"pageRanges": ["1-2", {}]}])
+def test_page_ranges_invalid(session, options):
+ response = do_print(session, options)
+ assert_error(response, "invalid argument")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/print/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/print/user_prompts.py
new file mode 100644
index 0000000000..ade1c38a5c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/print/user_prompts.py
@@ -0,0 +1,108 @@
+# META: timeout=long
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_pdf, assert_success
+from . import do_print
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = do_print(session, {})
+ value = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_pdf(value)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = do_print(session, {})
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = do_print(session, {})
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/refresh/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/refresh/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/refresh/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/refresh/refresh.py b/testing/web-platform/tests/webdriver/tests/classic/refresh/refresh.py
new file mode 100644
index 0000000000..3f434a3012
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/refresh/refresh.py
@@ -0,0 +1,108 @@
+import pytest
+
+from webdriver import error
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def refresh(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/refresh".format(**vars(session)))
+
+
+def test_null_response_value(session, inline):
+ session.url = inline("<div>")
+
+ response = refresh(session)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = refresh(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame, inline):
+ url = inline("<div id=foo>")
+
+ session.url = url
+ element = session.find.css("#foo", all=False)
+
+ response = refresh(session)
+ assert_success(response)
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.property("id")
+
+ assert session.url == url
+ assert session.find.css("#foo", all=False)
+
+
+@pytest.mark.parametrize("protocol,parameters", [
+ ("http", ""),
+ ("https", ""),
+ ("https", {"pipe": "header(Cross-Origin-Opener-Policy,same-origin)"})
+], ids=["http", "https", "https coop"])
+def test_seen_nodes(session, get_test_page, protocol, parameters):
+ page = get_test_page(parameters=parameters, protocol=protocol)
+
+ session.url = page
+
+ element = session.find.css("#custom-element", all=False)
+ shadow_root = element.shadow_root
+
+ response = refresh(session)
+ assert_success(response)
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.name
+ with pytest.raises(error.DetachedShadowRootException):
+ shadow_root.find_element("css selector", "in-shadow-dom")
+
+ session.find.css("#custom-element", all=False)
+
+
+def test_history_pushstate(session, inline):
+ pushstate_page = inline("""
+ <script>
+ function pushState() {
+ history.pushState({foo: "bar"}, "", "#pushstate");
+ }
+ </script>
+ <a onclick="javascript:pushState();">click</a>
+ """)
+
+ session.url = pushstate_page
+
+ session.find.css("a", all=False).click()
+ assert session.url == "{}#pushstate".format(pushstate_page)
+ assert session.execute_script("return history.state;") == {"foo": "bar"}
+
+ session.execute_script("""
+ let elem = window.document.createElement('div');
+ window.document.body.appendChild(elem);
+ """)
+ element = session.find.css("div", all=False)
+
+ response = refresh(session)
+ assert_success(response)
+
+ assert session.url == "{}#pushstate".format(pushstate_page)
+ assert session.execute_script("return history.state;") == {"foo": "bar"}
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.property("id")
+
+
+def test_refresh_switches_to_parent_browsing_context(session, create_frame, inline):
+ session.url = inline("<div id=foo>")
+
+ session.switch_frame(create_frame())
+ with pytest.raises(error.NoSuchElementException):
+ session.find.css("#foo", all=False)
+
+ response = refresh(session)
+ assert_success(response)
+
+ session.find.css("#foo", all=False)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/refresh/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/refresh/user_prompts.py
new file mode 100644
index 0000000000..5787533b57
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/refresh/user_prompts.py
@@ -0,0 +1,189 @@
+# META: timeout=long
+
+import pytest
+
+from webdriver import error
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def refresh(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/refresh".format(**vars(session)))
+
+
+@pytest.fixture
+def check_beforeunload_implicitly_accepted(session, url):
+ def check_beforeunload_implicitly_accepted():
+ page_beforeunload = url(
+ "/webdriver/tests/support/html/beforeunload.html")
+
+ session.url = page_beforeunload
+ element = session.find.css("input", all=False)
+ element.send_keys("bar")
+
+ response = refresh(session)
+ assert_success(response)
+
+ # navigation auto-dismissed beforeunload prompt
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.property("id")
+
+ session.find.css("input", all=False)
+
+ return check_beforeunload_implicitly_accepted
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<div id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = refresh(session)
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ with pytest.raises(error.StaleElementReferenceException):
+ element.property("id")
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<div id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = refresh(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert element.property("id") == "foo"
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<div id=foo>")
+ element = session.find.css("#foo", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = refresh(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert element.property("id") == "foo"
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_accept(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_dismiss(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_without_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ # retval not testable for confirm and prompt because window is gone
+ check_user_prompt_closed_without_exception(dialog_type, None)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception, dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "beforeunload", "confirm", "prompt"])
+def test_ignore(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_not_closed_but_exception,
+ dialog_type
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("beforeunload", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(
+ check_beforeunload_implicitly_accepted,
+ check_user_prompt_closed_with_exception,
+ dialog_type,
+ retval
+):
+ if dialog_type == "beforeunload":
+ check_beforeunload_implicitly_accepted()
+ else:
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/release_actions/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/release_actions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/release_actions/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/release_actions/conftest.py b/testing/web-platform/tests/webdriver/tests/classic/release_actions/conftest.py
new file mode 100644
index 0000000000..8275efc23b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/release_actions/conftest.py
@@ -0,0 +1,40 @@
+import pytest
+
+
+@pytest.fixture
+def key_chain(session):
+ return session.actions.sequence("key", "keyboard_id")
+
+
+@pytest.fixture
+def mouse_chain(session):
+ return session.actions.sequence(
+ "pointer",
+ "pointer_id",
+ {"pointerType": "mouse"})
+
+
+@pytest.fixture
+def none_chain(session):
+ return session.actions.sequence("none", "none_id")
+
+
+@pytest.fixture(autouse=True)
+def release_actions(session, request):
+ # release all actions after each test
+ # equivalent to a teardown_function, but with access to session fixture
+ request.addfinalizer(session.actions.release)
+
+
+@pytest.fixture
+def key_reporter(session, test_actions_page, request):
+ """Represents focused input element from `test_actions_page` fixture."""
+ input_el = session.find.css("#keys", all=False)
+ input_el.click()
+ session.execute_script("resetEvents();")
+ return input_el
+
+
+@pytest.fixture
+def test_actions_page(session, url):
+ session.url = url("/webdriver/tests/support/html/test_actions.html")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/release_actions/release.py b/testing/web-platform/tests/webdriver/tests/classic/release_actions/release.py
new file mode 100644
index 0000000000..5df1ff4be9
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/release_actions/release.py
@@ -0,0 +1,23 @@
+from tests.support.asserts import assert_error, assert_success
+
+
+def release_actions(session):
+ return session.transport.send(
+ "DELETE",
+ "/session/{session_id}/actions".format(**vars(session)),
+ )
+
+
+def test_null_response_value(session):
+ response = release_actions(session)
+ assert_success(response, None)
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = release_actions(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = release_actions(session)
+ assert_error(response, "no such window")
diff --git a/testing/web-platform/tests/webdriver/tests/classic/release_actions/sequence.py b/testing/web-platform/tests/webdriver/tests/classic/release_actions/sequence.py
new file mode 100644
index 0000000000..348f816946
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/release_actions/sequence.py
@@ -0,0 +1,66 @@
+import pytest
+
+from tests.classic.release_actions.support.refine import get_events, get_keys
+from tests.support.helpers import filter_dict, filter_supported_key_events
+
+
+def test_release_no_actions_sends_no_events(session, key_reporter):
+ session.actions.release()
+ assert len(get_keys(key_reporter)) == 0
+ assert len(get_events(session)) == 0
+
+
+def test_release_char_sequence_sends_keyup_events_in_reverse(session,
+ key_reporter,
+ key_chain):
+ key_chain \
+ .key_down("a") \
+ .key_down("b") \
+ .perform()
+ # reset so we only see the release events
+ session.execute_script("resetEvents();")
+ session.actions.release()
+ expected = [
+ {"code": "KeyB", "key": "b", "type": "keyup"},
+ {"code": "KeyA", "key": "a", "type": "keyup"},
+ ]
+ all_events = get_events(session)
+ (events, expected) = filter_supported_key_events(all_events, expected)
+ assert events == expected
+
+
+@pytest.mark.parametrize(
+ "release_actions",
+ [True, False],
+ ids=["with release actions", "without release actions"],
+)
+def test_release_mouse_sequence_resets_dblclick_state(session,
+ test_actions_page,
+ mouse_chain,
+ release_actions):
+ reporter = session.find.css("#outer", all=False)
+
+ mouse_chain \
+ .click(element=reporter) \
+ .perform()
+
+ if release_actions:
+ session.actions.release()
+
+ mouse_chain \
+ .perform()
+ events = get_events(session)
+
+ # The expeced data here might vary between the vendors since the spec at the moment
+ # is not clear on how the double/triple click should be tracked. It should be
+ # clarified in the scope of https://github.com/w3c/webdriver/issues/1772.
+ expected = [
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ {"type": "mousedown", "button": 0},
+ {"type": "mouseup", "button": 0},
+ {"type": "click", "button": 0},
+ ]
+ filtered_events = [filter_dict(e, expected[0]) for e in events]
+ assert expected == filtered_events[1:]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/release_actions/support/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/release_actions/support/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/release_actions/support/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/release_actions/support/refine.py b/testing/web-platform/tests/webdriver/tests/classic/release_actions/support/refine.py
new file mode 100644
index 0000000000..6e2f4574f8
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/release_actions/support/refine.py
@@ -0,0 +1,24 @@
+def get_events(session):
+ """Return list of key events recorded in the test_actions_page fixture."""
+ events = session.execute_script("return allEvents.events;") or []
+ # `key` values in `allEvents` may be escaped (see `escapeSurrogateHalf` in
+ # test_keys_wdspec.html), so this converts them back into unicode literals.
+ for e in events:
+ # example: turn "U+d83d" (6 chars) into u"\ud83d" (1 char)
+ if "key" in e and e["key"].startswith(u"U+"):
+ key = e["key"]
+ hex_suffix = key[key.index("+") + 1:]
+ e["key"] = chr(int(hex_suffix, 16))
+ return events
+
+
+def get_keys(input_el):
+ """Get printable characters entered into `input_el`.
+
+ :param input_el: HTML input element.
+ """
+ rv = input_el.property("value")
+ if rv is None:
+ return ""
+ else:
+ return rv
diff --git a/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/conftest.py b/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/conftest.py
new file mode 100644
index 0000000000..b080761bde
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/conftest.py
@@ -0,0 +1,24 @@
+import pytest
+
+from webdriver.error import NoSuchAlertException, NoSuchWindowException
+
+
+@pytest.fixture(name="session")
+def fixture_session(capabilities, session):
+ """Prevent dialog rate limits by running the test in a new window."""
+ original_handle = session.window_handle
+ session.window_handle = session.new_window()
+
+ yield session
+
+ try:
+ session.alert.dismiss()
+ except NoSuchAlertException:
+ pass
+
+ try:
+ session.window.close()
+ except NoSuchWindowException:
+ pass
+
+ session.window_handle = original_handle
diff --git a/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/send.py b/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/send.py
new file mode 100644
index 0000000000..df218c803b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/send_alert_text/send.py
@@ -0,0 +1,94 @@
+import pytest
+
+from webdriver.error import NoSuchAlertException
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.sync import Poll
+
+
+@pytest.fixture
+def page(session, inline):
+ session.url = inline("""
+ <script>window.result = window.prompt('Enter Your Name: ', 'Name');</script>
+ """)
+
+
+def send_alert_text(session, text=None):
+ return session.transport.send(
+ "POST", "session/{session_id}/alert/text".format(**vars(session)),
+ {"text": text})
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/alert/text".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_null_response_value(session, page):
+ response = send_alert_text(session, "Federer")
+ value = assert_success(response)
+ assert value is None
+
+
+@pytest.mark.parametrize("text", [None, {}, [], 42, True])
+def test_invalid_input(session, page, text):
+ response = send_alert_text(session, text)
+ assert_error(response, "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = send_alert_text(session, "Federer")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = send_alert_text(session, "Federer")
+ assert_error(response, "no such alert")
+
+
+def test_no_user_prompt(session):
+ response = send_alert_text(session, "Federer")
+ assert_error(response, "no such alert")
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm"])
+def test_alert_element_not_interactable(session, inline, dialog_type):
+ session.url = inline("<script>window.{}('Hello');</script>".format(dialog_type))
+
+ response = send_alert_text(session, "Federer")
+ assert_error(response, "element not interactable")
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm"])
+def test_chained_alert_element_not_interactable(session, inline, dialog_type):
+ session.url = inline("<script>window.{}('Hello');</script>".format(dialog_type))
+ session.alert.accept()
+
+ session.url = inline("<script>window.{}('Hello');</script>".format(dialog_type))
+ response = send_alert_text(session, "Federer")
+ assert_error(response, "element not interactable")
+
+
+@pytest.mark.parametrize("text", ["", "Federer", " Fed erer ", "Fed\terer"])
+def test_send_alert_text(session, page, text):
+ send_response = send_alert_text(session, text)
+ assert_success(send_response)
+
+ session.alert.accept()
+
+ assert session.execute_script("return window.result") == text
+
+
+def test_unexpected_alert(session):
+ session.execute_script("setTimeout(function() { prompt('Hello'); }, 100);")
+ wait = Poll(
+ session,
+ timeout=5,
+ ignored_exceptions=NoSuchAlertException,
+ message="No user prompt with text 'Hello' detected")
+ wait.until(lambda s: s.alert.text == "Hello")
+
+ response = send_alert_text(session, "Federer")
+ assert_success(response)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/set.py b/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/set.py
new file mode 100644
index 0000000000..6620f4df2a
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/set.py
@@ -0,0 +1,95 @@
+import pytest
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def set_timeouts(session, timeouts):
+ return session.transport.send(
+ "POST", "session/{session_id}/timeouts".format(**vars(session)),
+ timeouts)
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/timeouts".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_null_response_value(session):
+ timeouts = {"implicit": 10, "pageLoad": 10, "script": 10}
+ response = set_timeouts(session, timeouts)
+ value = assert_success(response)
+ assert value is None
+
+
+@pytest.mark.parametrize("value", [1, "{}", False, []])
+def test_parameters_invalid(session, value):
+ response = set_timeouts(session, value)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [{}, {"a": 42}])
+def test_parameters_unknown_fields(session, value):
+ original = session.timeouts._get()
+
+ response = set_timeouts(session, value)
+ assert_success(response)
+
+ assert session.timeouts._get() == original
+
+
+def test_script_parameter_empty_no_change(session):
+ original = session.timeouts._get()
+
+ response = set_timeouts(session, {"implicit": 100})
+ assert_success(response)
+
+ assert session.timeouts._get()["script"] == original["script"]
+
+
+@pytest.mark.parametrize("typ", ["implicit", "pageLoad", "script"])
+@pytest.mark.parametrize("value", [0, 2.0, 2**53 - 1])
+def test_positive_integer(session, typ, value):
+ response = set_timeouts(session, {typ: value})
+ assert_success(response)
+
+ assert session.timeouts._get(typ) == value
+
+
+@pytest.mark.parametrize("typ", ["implicit", "pageLoad"])
+@pytest.mark.parametrize("value", [None, [], {}, False, "10"])
+def test_value_invalid_types(session, typ, value):
+ response = set_timeouts(session, {typ: value})
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("value", [[], {}, False, "10"])
+def test_value_invalid_types_for_script(session, value):
+ response = set_timeouts(session, {"script": value})
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("typ", ["implicit", "pageLoad", "script"])
+@pytest.mark.parametrize("value", [-1, 2.5, 2**53])
+def test_value_positive_integer(session, typ, value):
+ response = set_timeouts(session, {typ: value})
+ assert_error(response, "invalid argument")
+
+
+def test_set_all_fields(session):
+ timeouts = {"implicit": 10, "pageLoad": 20, "script": 30}
+ response = set_timeouts(session, timeouts)
+ assert_success(response)
+
+ assert session.timeouts.implicit == 10
+ assert session.timeouts.page_load == 20
+ assert session.timeouts.script == 30
+
+
+def test_script_value_null(session):
+ response = set_timeouts(session, {"script": None})
+ assert_success(response)
+
+ assert session.timeouts.script is None
diff --git a/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/user_prompts.py
new file mode 100644
index 0000000000..a98d87e9b2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/set_timeouts/user_prompts.py
@@ -0,0 +1,62 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_success
+
+
+def set_timeouts(session, timeouts):
+ return session.transport.send(
+ "POST", "session/{session_id}/timeouts".format(**vars(session)),
+ timeouts)
+
+
+@pytest.fixture
+def check_user_prompt_not_closed(session, create_dialog):
+ def check_user_prompt_not_closed(dialog_type):
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = set_timeouts(session, {"script": 100})
+ assert_success(response)
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.timeouts.script == 100
+
+ return check_user_prompt_not_closed
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_accept(check_user_prompt_not_closed, dialog_type):
+ check_user_prompt_not_closed(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_accept_and_notify(check_user_prompt_not_closed, dialog_type):
+ check_user_prompt_not_closed(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_dismiss(check_user_prompt_not_closed, dialog_type):
+ check_user_prompt_not_closed(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_dismiss_and_notify(check_user_prompt_not_closed, dialog_type):
+ check_user_prompt_not_closed(dialog_type)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed, dialog_type):
+ check_user_prompt_not_closed(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_default(check_user_prompt_not_closed, dialog_type):
+ check_user_prompt_not_closed(dialog_type)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/set.py b/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/set.py
new file mode 100644
index 0000000000..5295c44fdd
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/set.py
@@ -0,0 +1,459 @@
+# META: timeout=long
+
+# Longer timeout required due to a bug in Chrome:
+# https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+
+import pytest
+
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.helpers import document_hidden, is_fullscreen, is_maximized
+
+
+def set_window_rect(session, rect):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/rect".format(**vars(session)),
+ rect)
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/window/rect".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = set_window_rect(session, {})
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_window):
+ response = set_window_rect(session, {})
+ assert_error(response, "no such window")
+
+
+def test_response_payload(session):
+ response = set_window_rect(session, {"x": 400, "y": 400})
+ value = assert_success(response, session.window.rect)
+
+ assert isinstance(value, dict)
+ assert isinstance(value.get("x"), int)
+ assert isinstance(value.get("y"), int)
+ assert isinstance(value.get("width"), int)
+ assert isinstance(value.get("height"), int)
+
+
+@pytest.mark.parametrize("rect", [
+ {"width": "a"},
+ {"height": "b"},
+ {"width": "a", "height": "b"},
+ {"x": "a"},
+ {"y": "b"},
+ {"x": "a", "y": "b"},
+ {"width": "a", "height": "b", "x": "a", "y": "b"},
+
+ {"width": True},
+ {"height": False},
+ {"width": True, "height": False},
+ {"x": True},
+ {"y": False},
+ {"x": True, "y": False},
+ {"width": True, "height": False, "x": True, "y": False},
+
+ {"width": []},
+ {"height": []},
+ {"width": [], "height": []},
+ {"x": []},
+ {"y": []},
+ {"x": [], "y": []},
+ {"width": [], "height": [], "x": [], "y": []},
+
+ {"height": {}},
+ {"width": {}},
+ {"height": {}, "width": {}},
+ {"x": {}},
+ {"y": {}},
+ {"x": {}, "y": {}},
+ {"width": {}, "height": {}, "x": {}, "y": {}},
+])
+def test_invalid_types(session, rect):
+ response = set_window_rect(session, rect)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("rect", [
+ {"width": -1},
+ {"height": -2},
+ {"width": -1, "height": -2},
+])
+def test_invalid_values(session, rect):
+ response = set_window_rect(session, rect)
+ assert_error(response, "invalid argument")
+
+
+def test_restore_from_fullscreen(session):
+ assert not is_fullscreen(session)
+
+ original = session.window.rect
+ target_rect = {
+ "x": original["x"],
+ "y": original["y"],
+ "width": original["width"] + 50,
+ "height": original["height"] + 50
+ }
+
+ session.window.fullscreen()
+ assert is_fullscreen(session)
+
+ response = set_window_rect(session, target_rect)
+ value = assert_success(response, session.window.rect)
+
+ assert not is_fullscreen(session)
+ assert value == target_rect
+
+
+def test_restore_from_minimized(session):
+ assert not document_hidden(session)
+
+ original = session.window.rect
+ target_rect = {
+ "x": original["x"],
+ "y": original["y"],
+ "width": original["width"] + 50,
+ "height": original["height"] + 50
+ }
+
+ session.window.minimize()
+ assert document_hidden(session)
+
+ response = set_window_rect(session, target_rect)
+ value = assert_success(response, session.window.rect)
+
+ assert not document_hidden(session)
+ assert value == target_rect
+
+
+def test_restore_from_maximized(session):
+ assert not is_maximized(session)
+
+ original = session.window.rect
+ target_rect = {
+ "x": original["x"],
+ "y": original["y"],
+ "width": original["width"] + 50,
+ "height": original["height"] + 50
+ }
+
+ session.window.maximize()
+ assert is_maximized(session)
+
+ response = set_window_rect(session, target_rect)
+ value = assert_success(response, session.window.rect)
+
+ assert not is_maximized(session)
+ assert value == target_rect
+
+
+def test_x_y_floats(session):
+ response = set_window_rect(session, {"x": 150.5, "y": 250})
+ value = assert_success(response)
+ assert value["x"] == 150
+ assert value["y"] == 250
+
+ response = set_window_rect(session, {"x": 150, "y": 250.5})
+ value = assert_success(response, session.window.rect)
+ assert value["x"] == 150
+ assert value["y"] == 250
+
+
+def test_width_height_floats(session):
+ response = set_window_rect(session, {"width": 500.5, "height": 420})
+ value = assert_success(response, session.window.rect)
+ assert value["width"] == 500
+ assert value["height"] == 420
+
+ response = set_window_rect(session, {"width": 500, "height": 450.5})
+ value = assert_success(response, session.window.rect)
+ assert value["width"] == 500
+ assert value["height"] == 450
+
+
+@pytest.mark.parametrize("rect", [
+ {},
+
+ {"width": None},
+ {"height": None},
+ {"width": None, "height": None},
+
+ {"x": None},
+ {"y": None},
+ {"x": None, "y": None},
+
+ {"width": None, "x": None},
+ {"width": None, "y": None},
+ {"height": None, "x": None},
+ {"height": None, "Y": None},
+
+ {"width": None, "height": None, "x": None, "y": None},
+
+ {"width": 200},
+ {"height": 200},
+ {"x": 200},
+ {"y": 200},
+ {"width": 200, "x": 200},
+ {"height": 200, "x": 200},
+ {"width": 200, "y": 200},
+ {"height": 200, "y": 200},
+])
+def test_no_change(session, rect):
+ original = session.window.rect
+ response = set_window_rect(session, rect)
+ assert_success(response, original)
+
+
+def test_set_to_available_size(
+ session, available_screen_size, minimal_screen_position
+):
+ minimal_x, minimal_y = minimal_screen_position
+ available_width, available_height = available_screen_size
+ target_rect = {
+ "x": minimal_x,
+ "y": minimal_y,
+ "width": available_width,
+ "height": available_height,
+ }
+
+ response = set_window_rect(session, target_rect)
+ value = assert_success(response, session.window.rect)
+
+ assert value == target_rect
+
+
+def test_set_to_screen_size(
+ session, available_screen_size, minimal_screen_position, screen_size
+):
+ minimal_x, minimal_y = minimal_screen_position
+ available_width, available_height = available_screen_size
+ screen_width, screen_height = screen_size
+ target_rect = {
+ "x": minimal_x,
+ "y": minimal_y,
+ "width": screen_width,
+ "height": screen_height,
+ }
+
+ response = set_window_rect(session, target_rect)
+ value = assert_success(response, session.window.rect)
+
+ assert value["width"] >= available_width
+ assert value["width"] <= screen_width
+ assert value["height"] >= available_height
+ assert value["height"] <= screen_height
+
+
+def test_set_larger_than_screen_size(
+ session, available_screen_size, minimal_screen_position, screen_size
+):
+ minimal_x, minimal_y = minimal_screen_position
+ available_width, available_height = available_screen_size
+ screen_width, screen_height = screen_size
+ target_rect = {
+ "x": minimal_x,
+ "y": minimal_y,
+ "width": screen_width + 100,
+ "height": screen_height + 100,
+ }
+
+ response = set_window_rect(session, target_rect)
+ value = assert_success(response, session.window.rect)
+
+ assert value["width"] >= available_width
+ assert value["height"] >= available_height
+
+
+def test_set_smaller_than_minimum_browser_size(session):
+ original_width, original_height = session.window.size
+
+ # A window size of 10x10px shouldn't be supported by any browser.
+ response = set_window_rect(session, {"width": 10, "height": 10})
+ value = assert_success(response, session.window.rect)
+
+ assert value["width"] < original_width
+ assert value["width"] > 10
+ assert value["height"] < original_height
+ assert value["height"] > 10
+
+
+def test_height_width_as_current(session):
+ original = session.window.rect
+
+ response = set_window_rect(session, {
+ "width": original["width"],
+ "height": original["height"]
+ })
+ value = assert_success(response, session.window.rect)
+
+ assert value == original
+
+
+def test_height_as_current(session):
+ original = session.window.rect
+
+ response = set_window_rect(session, {
+ "width": original["width"] + 10,
+ "height": original["height"]
+ })
+ value = assert_success(response, session.window.rect)
+
+ assert value == {
+ "x": original["x"],
+ "y": original["y"],
+ "width": original["width"] + 10,
+ "height": original["height"]
+ }
+
+
+def test_width_as_current(session):
+ original = session.window.rect
+
+ response = set_window_rect(session, {
+ "width": original["width"],
+ "height": original["height"] + 10
+ })
+ value = assert_success(response, session.window.rect)
+
+ assert value == {
+ "x": original["x"],
+ "y": original["y"],
+ "width": original["width"],
+ "height": original["height"] + 10
+ }
+
+
+def test_x_y(session):
+ original = session.window.rect
+ response = set_window_rect(session, {
+ "x": original["x"] + 10,
+ "y": original["y"] + 10
+ })
+ value = assert_success(response, session.window.rect)
+
+ assert value == {
+ "x": original["x"] + 10,
+ "y": original["y"] + 10,
+ "width": original["width"],
+ "height": original["height"]
+ }
+
+
+def test_x_y_as_current(session):
+ original = session.window.rect
+
+ response = set_window_rect(session, {
+ "x": original["x"],
+ "y": original["y"]
+ })
+ value = assert_success(response, session.window.rect)
+
+ assert value == {
+ "x": original["x"],
+ "y": original["y"],
+ "width": original["width"],
+ "height": original["height"]
+ }
+
+
+def test_x_as_current(session):
+ original = session.window.rect
+
+ response = set_window_rect(session, {
+ "x": original["x"],
+ "y": original["y"] + 10
+ })
+ value = assert_success(response, session.window.rect)
+
+ assert value == {
+ "x": original["x"],
+ "y": original["y"] + 10,
+ "width": original["width"],
+ "height": original["height"]
+ }
+
+
+def test_y_as_current(session):
+ original = session.window.rect
+
+ response = set_window_rect(session, {
+ "x": original["x"] + 10,
+ "y": original["y"]
+ })
+ value = assert_success(response, session.window.rect)
+
+ assert value == {
+ "x": original["x"] + 10,
+ "y": original["y"],
+ "width": original["width"],
+ "height": original["height"]
+ }
+
+
+def test_negative_x_y(session, minimal_screen_position):
+ original = session.window.rect
+
+ response = set_window_rect(session, {"x": - 8, "y": - 8})
+ value = assert_success(response, session.window.rect)
+
+ os = session.capabilities["platformName"]
+ # certain WMs prohibit windows from being moved off-screen
+ if os == "linux":
+ assert value["x"] <= 0
+ assert value["y"] <= 0
+ assert value["width"] == original["width"]
+ assert value["height"] == original["height"]
+
+ # On macOS when not running headless, windows can only be moved off the
+ # screen on the horizontal axis. The system menu bar also blocks windows
+ # from being moved to (0,0).
+ elif os == "mac":
+ assert value["x"] == -8
+ assert value["y"] <= minimal_screen_position[1]
+ assert value["width"] == original["width"]
+ assert value["height"] == original["height"]
+
+ # It turns out that Windows is the only platform on which the
+ # window can be reliably positioned off-screen.
+ elif os == "windows":
+ assert value == {
+ "x": -8,
+ "y": -8,
+ "width": original["width"],
+ "height": original["height"]
+ }
+
+
+"""
+TODO(ato):
+
+ Disable test because the while statements are wrong.
+ To fix this properly we need to write an explicit wait utility.
+
+def test_resize_by_script(session):
+ # setting the window size by JS is asynchronous
+ # so we poll waiting for the results
+
+ size0 = session.window.size
+
+ session.execute_script("window.resizeTo(700, 800)")
+ size1 = session.window.size
+ while size0 == size1:
+ size1 = session.window.size
+ assert size1 == (700, 800)
+
+ session.execute_script("window.resizeTo(800, 900)")
+ size2 = session.window.size
+ while size1 == size2:
+ size2 = session.window.size
+ assert size2 == (800, 900)
+ assert size2 == {"width": 200, "height": 100}
+"""
diff --git a/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/user_prompts.py
new file mode 100644
index 0000000000..908a9d920f
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/set_window_rect/user_prompts.py
@@ -0,0 +1,121 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_success
+
+
+def set_window_rect(session, rect):
+ return session.transport.send(
+ "POST", "session/{session_id}/window/rect".format(**vars(session)),
+ rect)
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ original_rect = session.window.rect
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = set_window_rect(session, {
+ "x": original_rect["x"] + 10, "y": original_rect["y"] + 10})
+ assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.window.rect != original_rect
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ original_rect = session.window.rect
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = set_window_rect(session, {
+ "x": original_rect["x"] + 10, "y": original_rect["y"] + 10})
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert session.window.rect == original_rect
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ original_rect = session.window.rect
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = set_window_rect(session, {
+ "x": original_rect["x"] + 10, "y": original_rect["y"] + 10})
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ assert session.window.rect == original_rect
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/status/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/status/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/status/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/status/status.py b/testing/web-platform/tests/webdriver/tests/classic/status/status.py
new file mode 100644
index 0000000000..8c7ae22a67
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/status/status.py
@@ -0,0 +1,33 @@
+import json
+
+from tests.support.asserts import assert_success
+
+
+def get_status(session):
+ return session.transport.send("GET", "/status")
+
+
+def test_get_status_no_session(http):
+ with http.get("/status") as response:
+ # GET /status should never return an error
+ assert response.status == 200
+
+ parsed_obj = json.loads(response.read().decode("utf-8"))
+ value = parsed_obj["value"]
+
+ assert value["ready"] in [True, False]
+ assert isinstance(value["message"], str)
+
+
+def test_status_with_session_running_on_endpoint_node(session):
+ response = get_status(session)
+ value = assert_success(response)
+ assert value["ready"] is False
+ assert "message" in value
+
+ session.end()
+
+ response = get_status(session)
+ value = assert_success(response)
+ assert value["ready"] is True
+ assert "message" in value
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/cross_origin.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/cross_origin.py
new file mode 100644
index 0000000000..633eba3f42
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/cross_origin.py
@@ -0,0 +1,63 @@
+from urllib.parse import urlparse
+
+import webdriver.protocol as protocol
+
+from tests.support.asserts import assert_success
+from tests.support.helpers import document_location
+
+
+"""
+Tests that WebDriver can transcend site origins.
+
+Many modern browsers impose strict cross-origin checks,
+and WebDriver should be able to transcend these.
+
+Although an implementation detail, certain browsers
+also enforce process isolation based on site origin.
+This is known to sometimes cause problems for WebDriver implementations.
+"""
+
+
+def switch_to_frame(session, frame):
+ return session.transport.send(
+ "POST", "/session/{session_id}/frame".format(**vars(session)),
+ {"id": frame},
+ encoder=protocol.Encoder, decoder=protocol.Decoder,
+ session=session)
+
+
+def test_cross_origin_iframe(session, server_config, inline, iframe):
+ session.url = inline(iframe("", domain="alt"))
+ frame_element = session.find.css("iframe", all=False)
+
+ response = switch_to_frame(session, frame_element)
+ assert_success(response)
+
+ parse_result = urlparse(document_location(session))
+ assert parse_result.netloc != server_config["browser_host"]
+
+
+def test_nested_cross_origin_iframe(session, server_config, inline, iframe):
+ frame2 = iframe("", domain="alt", subdomain="www")
+ frame1 = iframe(frame2)
+ top_doc = inline(frame1, domain="alt")
+
+ session.url = top_doc
+
+ parse_result = urlparse(document_location(session))
+ top_level_host = parse_result.netloc
+ assert not top_level_host.startswith(server_config["browser_host"])
+
+ frame1_element = session.find.css("iframe", all=False)
+ response = switch_to_frame(session, frame1_element)
+ assert_success(response)
+
+ parse_result = urlparse(document_location(session))
+ assert parse_result.netloc.startswith(server_config["browser_host"])
+
+ frame2_el = session.find.css("iframe", all=False)
+ response = switch_to_frame(session, frame2_el)
+ assert_success(response)
+
+ parse_result = urlparse(document_location(session))
+ assert parse_result.netloc == "www.{}".format(top_level_host)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch.py
new file mode 100644
index 0000000000..b9cccb3ecc
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch.py
@@ -0,0 +1,125 @@
+import pytest
+
+import webdriver.protocol as protocol
+
+from webdriver import NoSuchElementException
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_same_element, assert_success
+
+
+def switch_to_frame(session, frame):
+ return session.transport.send(
+ "POST", "session/{session_id}/frame".format(**vars(session)),
+ {"id": frame},
+ encoder=protocol.Encoder, decoder=protocol.Decoder,
+ session=session)
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/frame".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_null_response_value(session, inline, iframe):
+ session.url = inline(iframe("<p>foo"))
+ frame = session.find.css("iframe", all=False)
+
+ response = switch_to_frame(session, frame)
+ value = assert_success(response)
+ assert value is None
+
+
+@pytest.mark.parametrize("id", [
+ None,
+ 0,
+ {"element-6066-11e4-a52e-4f735466cecf": "foo"},
+])
+def test_no_top_browsing_context(session, url, id):
+ session.window_handle = session.new_window()
+
+ session.url = url("/webdriver/tests/support/html/frames.html")
+
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ session.window.close()
+
+ response = switch_to_frame(session, id)
+ assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize("id", [
+ None,
+ 0,
+ {"element-6066-11e4-a52e-4f735466cecf": "foo"},
+])
+def test_no_browsing_context(session, closed_frame, id):
+ response = switch_to_frame(session, id)
+ if id is None:
+ assert_success(response)
+ session.find.css("#delete", all=False)
+ else:
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context_when_already_top_level(session, closed_window):
+ response = switch_to_frame(session, None)
+ assert_error(response, "no such window")
+
+
+@pytest.mark.parametrize("value", ["foo", True, [], {}])
+def test_frame_id_invalid_types(session, value):
+ response = switch_to_frame(session, value)
+ assert_error(response, "invalid argument")
+
+
+def test_frame_id_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = switch_to_frame(session, element.shadow_root)
+ assert_error(result, "invalid argument")
+
+
+def test_frame_id_null(session, inline, iframe):
+ session.url = inline(iframe("{}<div>foo".format(iframe("<p>bar"))))
+
+ frame1 = session.find.css("iframe", all=False)
+ session.switch_frame(frame1)
+ element1 = session.find.css("div", all=False)
+
+ frame2 = session.find.css("iframe", all=False)
+ session.switch_frame(frame2)
+ element2 = session.find.css("p", all=False)
+
+ # Switch to top-level browsing context
+ response = switch_to_frame(session, None)
+ assert_success(response)
+
+ with pytest.raises(NoSuchElementException):
+ element2.text
+ with pytest.raises(NoSuchElementException):
+ element1.text
+
+ frame = session.find.css("iframe", all=False)
+ assert_same_element(session, frame, frame1)
+
+
+def test_find_element_while_frame_is_still_loading(session, url):
+ session.timeouts.implicit = 5
+
+ frame_url = url("/webdriver/tests/support/html/subframe.html?pipe=trickle(d2)")
+ page_url = "<html><body><iframe src='{}'></iframe></body></html>".format(frame_url)
+
+ session.execute_script(
+ "document.documentElement.innerHTML = arguments[0];", args=[page_url])
+
+ frame1 = session.find.css("iframe", all=False)
+ session.switch_frame(frame1)
+
+ # Ensure that the is always a valid browsing context, and the element
+ # can be found eventually.
+ session.find.css("#delete", all=False)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_number.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_number.py
new file mode 100644
index 0000000000..c8858e77ff
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_number.py
@@ -0,0 +1,50 @@
+import pytest
+
+import webdriver.protocol as protocol
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def switch_to_frame(session, frame):
+ return session.transport.send(
+ "POST", "session/{session_id}/frame".format(**vars(session)),
+ {"id": frame},
+ encoder=protocol.Encoder, decoder=protocol.Decoder,
+ session=session)
+
+
+@pytest.mark.parametrize("value", [-1, 2**16])
+def test_frame_id_number_out_of_bounds(session, value):
+ response = switch_to_frame(session, value)
+ assert_error(response, "invalid argument")
+
+
+@pytest.mark.parametrize("index", [1, 65535])
+def test_frame_id_number_index_out_of_bounds(session, inline, iframe, index):
+ session.url = inline(iframe("<p>foo"))
+
+ response = switch_to_frame(session, index)
+ assert_error(response, "no such frame")
+
+
+@pytest.mark.parametrize("index, value", [[0, "foo"], [1, "bar"]])
+def test_frame_id_number_index(session, inline, iframe, index, value):
+ session.url = inline("{}{}".format(iframe("<p>foo"), iframe("<p>bar")))
+
+ response = switch_to_frame(session, index)
+ assert_success(response)
+
+ element = session.find.css("p", all=False)
+ assert element.text == value
+
+
+def test_frame_id_number_index_nested(session, inline, iframe):
+ session.url = inline(iframe("{}<p>foo".format(iframe("<p>bar"))))
+
+ expected_text = ["foo", "bar"]
+ for i in range(0, len(expected_text)):
+ response = switch_to_frame(session, 0)
+ assert_success(response)
+
+ element = session.find.css("p", all=False)
+ assert element.text == expected_text[i]
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_webelement.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_webelement.py
new file mode 100644
index 0000000000..ceadccd812
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_frame/switch_webelement.py
@@ -0,0 +1,100 @@
+import pytest
+
+import webdriver.protocol as protocol
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def switch_to_frame(session, frame):
+ return session.transport.send(
+ "POST", "session/{session_id}/frame".format(**vars(session)),
+ {"id": frame},
+ encoder=protocol.Encoder, decoder=protocol.Decoder,
+ session=session)
+
+
+def frameset(inline, *docs):
+ frames = list(map(lambda doc: "<frame src='{}'></frame>".format(inline(doc)), docs))
+ return "<frameset rows='{}'>\n{}</frameset>".format(len(frames) * "*,", "\n".join(frames))
+
+
+def test_frame_id_webelement_no_such_element(session, iframe, inline):
+ session.url = inline(iframe("<p>foo"))
+ frame = session.find.css("iframe", all=False)
+ frame.id = "bar"
+
+ response = switch_to_frame(session, frame)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_frame_id_webelement_stale_element_reference(session, stale_element, as_frame):
+ frame = stale_element("iframe", as_frame=as_frame)
+
+ result = switch_to_frame(session, frame)
+ assert_error(result, "stale element reference")
+
+
+def test_frame_id_webelement_no_frame_element(session, inline):
+ session.url = inline("<p>foo")
+ no_frame = session.find.css("p", all=False)
+
+ response = switch_to_frame(session, no_frame)
+ assert_error(response, "no such frame")
+
+
+@pytest.mark.parametrize("index, value", [[0, "foo"], [1, "bar"]])
+def test_frame_id_webelement_frame(session, inline, index, value):
+ session.url = inline(frameset(inline, "<p>foo", "<p>bar"))
+ frames = session.find.css("frame")
+ assert len(frames) == 2
+
+ response = switch_to_frame(session, frames[index])
+ assert_success(response)
+
+ element = session.find.css("p", all=False)
+ assert element.text == value
+
+
+@pytest.mark.parametrize("index, value", [[0, "foo"], [1, "bar"]])
+def test_frame_id_webelement_iframe(session, inline, iframe, index, value):
+ session.url = inline("{}{}".format(iframe("<p>foo"), iframe("<p>bar")))
+ frames = session.find.css("iframe")
+ assert len(frames) == 2
+
+ response = switch_to_frame(session, frames[index])
+ assert_success(response)
+
+ element = session.find.css("p", all=False)
+ assert element.text == value
+
+
+def test_frame_id_webelement_nested(session, inline, iframe):
+ session.url = inline(iframe("{}<p>foo".format(iframe("<p>bar"))))
+
+ expected_text = ["foo", "bar"]
+ for i in range(0, len(expected_text)):
+ frame_element = session.find.css("iframe", all=False)
+ response = switch_to_frame(session, frame_element)
+ assert_success(response)
+
+ element = session.find.css("p", all=False)
+ assert element.text == expected_text[i]
+
+
+def test_frame_id_webelement_cloned_into_iframe(session, inline, iframe):
+ session.url = inline(iframe("<body><p>hello world</p></body>"))
+
+ session.execute_script("""
+ const iframe = document.getElementsByTagName('iframe')[0];
+ const div = document.createElement('div');
+ div.innerHTML = 'I am a div created in top window and appended into the iframe';
+ iframe.contentWindow.document.body.appendChild(div);
+ """)
+
+ frame = session.find.css("iframe", all=False)
+ response = switch_to_frame(session, frame)
+ assert_success(response)
+
+ element = session.find.css("div", all=False)
+ assert element.text == "I am a div created in top window and appended into the iframe"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/switch.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/switch.py
new file mode 100644
index 0000000000..f777d6a767
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_parent_frame/switch.py
@@ -0,0 +1,101 @@
+import pytest
+
+from webdriver import NoSuchElementException, NoSuchWindowException
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.sync import Poll
+
+
+def switch_to_parent_frame(session):
+ return session.transport.send(
+ "POST", "session/{session_id}/frame/parent".format(**vars(session)))
+
+
+def test_null_response_value(session, inline, iframe):
+ session.url = inline(iframe("<p>foo"))
+ frame_element = session.find.css("iframe", all=False)
+ session.switch_frame(frame_element)
+
+ response = switch_to_parent_frame(session)
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session, url):
+ session.window_handle = session.new_window()
+
+ session.url = url("/webdriver/tests/support/html/frames.html")
+
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ session.window.close()
+
+ response = switch_to_parent_frame(session)
+ assert_error(response, "no such window")
+
+
+def test_no_parent_browsing_context(session, url):
+ session.url = url("/webdriver/tests/support/html/frames.html")
+
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ deleteframe = session.find.css("#delete-frame", all=False)
+ session.switch_frame(deleteframe)
+
+ button = session.find.css("#remove-top", all=False)
+ button.click()
+
+ def is_window_closed(s):
+ try:
+ s.find.css("#remove-top", all=False)
+ return False
+ except NoSuchWindowException:
+ return True
+
+ # Wait until iframe is gone.
+ wait = Poll(
+ session,
+ timeout=5,
+ message="Iframe is still present",
+ )
+ wait.until(lambda s: is_window_closed(s))
+
+ response = switch_to_parent_frame(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame):
+ response = switch_to_parent_frame(session)
+ assert_success(response)
+
+ session.find.css("#delete", all=False)
+
+
+def test_no_browsing_context_when_already_top_level(session, closed_window):
+ response = switch_to_parent_frame(session)
+ assert_error(response, "no such window")
+
+
+def test_switch_from_iframe(session, inline, iframe):
+ session.url = inline(iframe("<p>foo"))
+ frame_element = session.find.css("iframe", all=False)
+ session.switch_frame(frame_element)
+ element = session.find.css("p", all=False)
+
+ result = switch_to_parent_frame(session)
+ assert_success(result)
+
+ with pytest.raises(NoSuchElementException):
+ element.text
+
+
+def test_switch_from_top_level(session, inline):
+ session.url = inline("<p>foo")
+ element = session.find.css("p", all=False)
+
+ result = switch_to_parent_frame(session)
+ assert_success(result)
+
+ assert element.text == "foo"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/alerts.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/alerts.py
new file mode 100644
index 0000000000..2fc390e864
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/alerts.py
@@ -0,0 +1,33 @@
+import pytest
+
+from webdriver import error
+
+from tests.support.asserts import assert_success
+
+
+def switch_to_window(session, handle):
+ return session.transport.send(
+ "POST", "session/{session_id}/window".format(**vars(session)),
+ {"handle": handle})
+
+
+def test_retain_tab_modal_status(session):
+ handle = session.window_handle
+
+ new_handle = session.new_window()
+ response = switch_to_window(session, new_handle)
+ assert_success(response)
+
+ session.execute_script("window.alert('Hello');")
+ assert session.alert.text == "Hello"
+
+ response = switch_to_window(session, handle)
+ assert_success(response)
+
+ with pytest.raises(error.NoSuchAlertException):
+ session.alert.text == "Hello"
+
+ response = switch_to_window(session, new_handle)
+ assert_success(response)
+
+ assert session.alert.text == "Hello"
diff --git a/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/switch.py b/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/switch.py
new file mode 100644
index 0000000000..28d432a8b5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/switch_to_window/switch.py
@@ -0,0 +1,100 @@
+import pytest
+
+from webdriver.error import NoSuchElementException, NoSuchAlertException
+from webdriver.transport import Response
+
+from tests.support.asserts import assert_error, assert_success
+
+
+def switch_to_window(session, handle):
+ return session.transport.send(
+ "POST", "session/{session_id}/window".format(**vars(session)),
+ {"handle": handle})
+
+
+def test_null_parameter_value(session, http):
+ path = "/session/{session_id}/window".format(**vars(session))
+ with http.post(path, None) as response:
+ assert_error(Response.from_http(response), "invalid argument")
+
+
+def test_null_response_value(session):
+ response = switch_to_window(session, session.new_window())
+ value = assert_success(response)
+ assert value is None
+
+
+def test_no_top_browsing_context(session):
+ original_handle = session.window_handle
+ new_handle = session.new_window()
+
+ session.window.close()
+ assert original_handle not in session.handles, "Unable to close window"
+
+ response = switch_to_window(session, new_handle)
+ assert_success(response)
+
+ assert session.window_handle == new_handle
+
+
+def test_no_browsing_context(session, url):
+ new_handle = session.new_window()
+
+ session.url = url("/webdriver/tests/support/html/frames.html")
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ deleteframe = session.find.css("#delete-frame", all=False)
+ session.switch_frame(deleteframe)
+
+ button = session.find.css("#remove-parent", all=False)
+ button.click()
+
+ response = switch_to_window(session, new_handle)
+ assert_success(response)
+
+ assert session.window_handle == new_handle
+
+
+def test_switch_to_window_sets_top_level_context(session, inline, iframe):
+ session.url = inline(iframe("<p>foo"))
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+ session.find.css("p", all=False)
+
+ response = switch_to_window(session, session.window_handle)
+ assert_success(response)
+
+ session.find.css("iframe", all=False)
+
+
+def test_element_not_found_after_tab_switch(session, inline):
+ session.url = inline("<p id='a'>foo")
+ paragraph = session.find.css("p", all=False)
+
+ session.window_handle = session.new_window(type_hint="tab")
+
+ with pytest.raises(NoSuchElementException):
+ paragraph.attribute("id")
+
+
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_finds_exising_user_prompt_after_tab_switch(session, dialog_type):
+ original_handle = session.window_handle
+ new_handle = session.new_window()
+
+ session.execute_script("{}('foo');".format(dialog_type))
+
+ response = switch_to_window(session, new_handle)
+ assert_success(response)
+
+ with pytest.raises(NoSuchAlertException):
+ session.alert.text
+
+ session.window.close()
+
+ response = switch_to_window(session, original_handle)
+ assert_success(response)
+
+ session.alert.accept()
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/__init__.py
new file mode 100644
index 0000000000..9a82cc48ea
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/__init__.py
@@ -0,0 +1,10 @@
+def element_dimensions(session, element):
+ return tuple(session.execute_script("""
+ const {devicePixelRatio} = window;
+ let {width, height} = arguments[0].getBoundingClientRect();
+
+ return [
+ Math.floor(width * devicePixelRatio),
+ Math.floor(height * devicePixelRatio),
+ ];
+ """, args=(element,)))
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/iframe.py b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/iframe.py
new file mode 100644
index 0000000000..e7f1b0c805
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/iframe.py
@@ -0,0 +1,121 @@
+import pytest
+
+from tests.support.asserts import assert_success
+from tests.support.image import png_dimensions
+
+from . import element_dimensions
+
+DEFAULT_CONTENT = "<div id='content'>Lorem ipsum dolor sit amet.</div>"
+
+REFERENCE_CONTENT = "<div id='outer'>{}</div>".format(DEFAULT_CONTENT)
+REFERENCE_STYLE = """
+ <style>
+ #outer {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 200px;
+ height: 200px;
+ }
+ #content {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ background: green;
+ }
+ </style>
+"""
+
+OUTER_IFRAME_STYLE = """
+ <style>
+ iframe {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 200px;
+ height: 200px;
+ }
+ </style>
+"""
+
+INNER_IFRAME_STYLE = """
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ background: green;
+ }
+ </style>
+"""
+
+
+def take_element_screenshot(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/screenshot".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ )
+ )
+
+
+def test_frame_element(session, inline, iframe):
+ # Create a reference element which looks exactly like the frame's content
+ session.url = inline("{0}{1}".format(REFERENCE_STYLE, REFERENCE_CONTENT))
+
+ # Capture the inner content as reference image
+ ref_el = session.find.css("#content", all=False)
+ ref_screenshot = ref_el.screenshot()
+ ref_dimensions = element_dimensions(session, ref_el)
+
+ assert png_dimensions(ref_screenshot) == ref_dimensions
+
+ # Capture the frame's element
+ iframe_content = "{0}{1}".format(INNER_IFRAME_STYLE, DEFAULT_CONTENT)
+ session.url = inline("""{0}{1}""".format(OUTER_IFRAME_STYLE, iframe(iframe_content)))
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+ div = session.find.css("div", all=False)
+ div_dimensions = element_dimensions(session, div)
+ assert div_dimensions == ref_dimensions
+
+ response = take_element_screenshot(session, div.id)
+ div_screenshot = assert_success(response)
+
+ assert png_dimensions(div_screenshot) == ref_dimensions
+ assert div_screenshot == ref_screenshot
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+def test_source_origin(session, inline, iframe, domain):
+ # Create a reference element which looks exactly like the iframe
+ session.url = inline("{0}{1}".format(REFERENCE_STYLE, REFERENCE_CONTENT))
+
+ div = session.find.css("div", all=False)
+ div_dimensions = element_dimensions(session, div)
+
+ response = take_element_screenshot(session, div.id)
+ reference_screenshot = assert_success(response)
+ assert png_dimensions(reference_screenshot) == div_dimensions
+
+ iframe_content = "{0}{1}".format(INNER_IFRAME_STYLE, DEFAULT_CONTENT)
+ session.url = inline("""{0}{1}""".format(
+ OUTER_IFRAME_STYLE, iframe(iframe_content, domain=domain)))
+
+ frame_element = session.find.css("iframe", all=False)
+ frame_dimensions = element_dimensions(session, frame_element)
+
+ response = take_element_screenshot(session, frame_element.id)
+ screenshot = assert_success(response)
+ assert png_dimensions(screenshot) == frame_dimensions
+
+ assert screenshot == reference_screenshot
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/screenshot.py b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/screenshot.py
new file mode 100644
index 0000000000..fdc0d65b1d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/screenshot.py
@@ -0,0 +1,100 @@
+import pytest
+
+from webdriver import WebElement
+
+from tests.support.asserts import assert_error, assert_success
+from tests.support.image import png_dimensions
+from . import element_dimensions
+
+
+def take_element_screenshot(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/screenshot".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ )
+ )
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = take_element_screenshot(session, "foo")
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ response = take_element_screenshot(session, element.id)
+ screenshot = assert_success(response)
+
+ assert png_dimensions(screenshot) == element_dimensions(session, element)
+
+
+def test_no_such_element_with_invalid_value(session):
+ element = WebElement(session, "foo")
+
+ response = take_element_screenshot(session, element.id)
+ assert_error(response, "no such element")
+
+
+def test_no_such_element_with_shadow_root(session, get_test_page):
+ session.url = get_test_page()
+
+ element = session.find.css("custom-element", all=False)
+
+ result = take_element_screenshot(session, element.shadow_root.id)
+ assert_error(result, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_window_handle(session, inline, closed):
+ session.url = inline("<div id='parent'><p/>")
+ element = session.find.css("#parent", all=False)
+
+ new_handle = session.new_window()
+
+ if closed:
+ session.window.close()
+
+ session.window_handle = new_handle
+
+ response = take_element_screenshot(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("closed", [False, True], ids=["open", "closed"])
+def test_no_such_element_from_other_frame(session, get_test_page, closed):
+ session.url = get_test_page(as_frame=True)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ element = session.find.css("div", all=False)
+
+ session.switch_frame("parent")
+
+ if closed:
+ session.execute_script("arguments[0].remove();", args=[frame])
+
+ response = take_element_screenshot(session, element.id)
+ assert_error(response, "no such element")
+
+
+@pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"])
+def test_stale_element_reference(session, stale_element, as_frame):
+ element = stale_element("input#text", as_frame=as_frame)
+
+ result = take_element_screenshot(session, element.id)
+ assert_error(result, "stale element reference")
+
+
+def test_format_and_dimensions(session, inline):
+ session.url = inline("<input>")
+ element = session.find.css("input", all=False)
+
+ response = take_element_screenshot(session, element.id)
+ screenshot = assert_success(response)
+
+ assert png_dimensions(screenshot) == element_dimensions(session, element)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/user_prompts.py
new file mode 100644
index 0000000000..39fefe9325
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_element_screenshot/user_prompts.py
@@ -0,0 +1,121 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_png, assert_success
+
+
+def take_element_screenshot(session, element_id):
+ return session.transport.send(
+ "GET",
+ "session/{session_id}/element/{element_id}/screenshot".format(
+ session_id=session.session_id,
+ element_id=element_id,
+ )
+ )
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input/>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = take_element_screenshot(session, element.id)
+ value = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_png(value)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input/>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = take_element_screenshot(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input/>")
+ element = session.find.css("input", all=False)
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = take_element_screenshot(session, element.id)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/__init__.py b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/__init__.py
new file mode 100644
index 0000000000..f3001d946d
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/__init__.py
@@ -0,0 +1,21 @@
+def element_dimensions(session, element):
+ return tuple(session.execute_script("""
+ const {devicePixelRatio} = window;
+ let {width, height} = arguments[0].getBoundingClientRect();
+
+ return [
+ Math.floor(width * devicePixelRatio),
+ Math.floor(height * devicePixelRatio),
+ ];
+ """, args=(element,)))
+
+
+def viewport_dimensions(session):
+ return tuple(session.execute_script("""
+ const {devicePixelRatio, innerHeight, innerWidth} = window;
+
+ return [
+ Math.floor(innerWidth * devicePixelRatio),
+ Math.floor(innerHeight * devicePixelRatio)
+ ];
+ """))
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/iframe.py b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/iframe.py
new file mode 100644
index 0000000000..133692bc7e
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/iframe.py
@@ -0,0 +1,54 @@
+import pytest
+from tests.support.asserts import assert_success
+from tests.support.image import png_dimensions
+from tests.support.screenshot import (
+ DEFAULT_CONTENT,
+ INNER_IFRAME_STYLE,
+ OUTER_IFRAME_STYLE,
+ REFERENCE_CONTENT,
+ REFERENCE_STYLE,
+)
+
+from . import viewport_dimensions
+
+
+def take_screenshot(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/screenshot".format(**vars(session)))
+
+
+def test_always_captures_top_browsing_context(session, inline, iframe):
+ iframe_content = "{0}{1}".format(INNER_IFRAME_STYLE, DEFAULT_CONTENT)
+ session.url = inline("""{0}{1}""".format(OUTER_IFRAME_STYLE, iframe(iframe_content)))
+
+ response = take_screenshot(session)
+ reference_screenshot = assert_success(response)
+ assert png_dimensions(reference_screenshot) == viewport_dimensions(session)
+
+ frame = session.find.css("iframe", all=False)
+ session.switch_frame(frame)
+
+ response = take_screenshot(session)
+ screenshot = assert_success(response)
+
+ assert png_dimensions(screenshot) == png_dimensions(reference_screenshot)
+ assert screenshot == reference_screenshot
+
+
+@pytest.mark.parametrize("domain", ["", "alt"], ids=["same_origin", "cross_origin"])
+def test_source_origin(session, inline, iframe, domain):
+ session.url = inline("{0}{1}".format(REFERENCE_STYLE, REFERENCE_CONTENT))
+
+ response = take_screenshot(session)
+ reference_screenshot = assert_success(response)
+ assert png_dimensions(reference_screenshot) == viewport_dimensions(session)
+
+ iframe_content = "{0}{1}".format(INNER_IFRAME_STYLE, DEFAULT_CONTENT)
+ session.url = inline("""{0}{1}""".format(
+ OUTER_IFRAME_STYLE, iframe(iframe_content, domain=domain)))
+
+ response = take_screenshot(session)
+ screenshot = assert_success(response)
+ assert png_dimensions(screenshot) == viewport_dimensions(session)
+
+ assert screenshot == reference_screenshot
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/screenshot.py b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/screenshot.py
new file mode 100644
index 0000000000..9e71a633c7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/screenshot.py
@@ -0,0 +1,34 @@
+from tests.support.asserts import assert_error, assert_png, assert_success
+from tests.support.image import png_dimensions
+
+from . import viewport_dimensions
+
+
+def take_screenshot(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/screenshot".format(**vars(session)))
+
+
+def test_no_top_browsing_context(session, closed_window):
+ response = take_screenshot(session)
+ assert_error(response, "no such window")
+
+
+def test_no_browsing_context(session, closed_frame, inline):
+ session.url = inline("<input>")
+
+ response = take_screenshot(session)
+ value = assert_success(response)
+
+ assert_png(value)
+ assert png_dimensions(value) == viewport_dimensions(session)
+
+
+def test_format_and_dimensions(session, inline):
+ session.url = inline("<input>")
+
+ response = take_screenshot(session)
+ value = assert_success(response)
+
+ assert_png(value)
+ assert png_dimensions(value) == viewport_dimensions(session)
diff --git a/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/user_prompts.py b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/user_prompts.py
new file mode 100644
index 0000000000..7d57f8f271
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/classic/take_screenshot/user_prompts.py
@@ -0,0 +1,113 @@
+# META: timeout=long
+
+import pytest
+
+from tests.support.asserts import assert_dialog_handled, assert_error, assert_png, assert_success
+
+
+def take_screenshot(session):
+ return session.transport.send(
+ "GET", "session/{session_id}/screenshot".format(**vars(session)))
+
+
+@pytest.fixture
+def check_user_prompt_closed_without_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_without_exception(dialog_type, retval):
+ session.url = inline("<input/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = take_screenshot(session)
+ value = assert_success(response)
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ assert_png(value)
+
+ return check_user_prompt_closed_without_exception
+
+
+@pytest.fixture
+def check_user_prompt_closed_with_exception(session, create_dialog, inline):
+ def check_user_prompt_closed_with_exception(dialog_type, retval):
+ session.url = inline("<input/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = take_screenshot(session)
+ assert_error(response, "unexpected alert open")
+
+ assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)
+
+ return check_user_prompt_closed_with_exception
+
+
+@pytest.fixture
+def check_user_prompt_not_closed_but_exception(session, create_dialog, inline):
+ def check_user_prompt_not_closed_but_exception(dialog_type):
+ session.url = inline("<input/>")
+
+ create_dialog(dialog_type, text=dialog_type)
+
+ response = take_screenshot(session)
+ assert_error(response, "unexpected alert open")
+
+ assert session.alert.text == dialog_type
+ session.alert.dismiss()
+
+ return check_user_prompt_not_closed_but_exception
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", True),
+ ("prompt", ""),
+])
+def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
+ check_user_prompt_closed_without_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
+
+
+@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
+@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
+def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
+ check_user_prompt_not_closed_but_exception(dialog_type)
+
+
+@pytest.mark.parametrize("dialog_type, retval", [
+ ("alert", None),
+ ("confirm", False),
+ ("prompt", None),
+])
+def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
+ check_user_prompt_closed_with_exception(dialog_type, retval)
diff --git a/testing/web-platform/tests/webdriver/tests/conftest.py b/testing/web-platform/tests/webdriver/tests/conftest.py
new file mode 100644
index 0000000000..fe9f5cd268
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/conftest.py
@@ -0,0 +1,5 @@
+pytest_plugins = (
+ "tests.support.fixtures",
+ "tests.support.fixtures_bidi",
+ "tests.support.fixtures_http",
+)
diff --git a/testing/web-platform/tests/webdriver/tests/support/__init__.py b/testing/web-platform/tests/webdriver/tests/support/__init__.py
new file mode 100644
index 0000000000..0535edd214
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/__init__.py
@@ -0,0 +1,12 @@
+import sys
+
+platform_name = {
+ # From Python version 3.3: On Linux, sys.platform doesn't contain the major version anymore.
+ # It is always 'linux'. See
+ # https://docs.python.org/3/library/sys.html#sys.platform
+ "linux": "linux",
+ "linux2": "linux",
+ "win32": "windows",
+ "cygwin": "windows",
+ "darwin": "mac"
+}.get(sys.platform)
diff --git a/testing/web-platform/tests/webdriver/tests/support/asserts.py b/testing/web-platform/tests/webdriver/tests/support/asserts.py
new file mode 100644
index 0000000000..f9d5da5217
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/asserts.py
@@ -0,0 +1,231 @@
+import imghdr
+from base64 import decodebytes
+
+from webdriver import NoSuchAlertException, WebDriverException, WebElement
+
+# WebDriver specification ID: dfn-error-response-data
+errors = {
+ "detached shadow root": 404,
+ "element click intercepted": 400,
+ "element not selectable": 400,
+ "element not interactable": 400,
+ "insecure certificate": 400,
+ "invalid argument": 400,
+ "invalid cookie domain": 400,
+ "invalid coordinates": 400,
+ "invalid element state": 400,
+ "invalid selector": 400,
+ "invalid session id": 404,
+ "javascript error": 500,
+ "move target out of bounds": 500,
+ "no such alert": 404,
+ "no such cookie": 404,
+ "no such element": 404,
+ "no such frame": 404,
+ "no such shadow root": 404,
+ "no such window": 404,
+ "script timeout": 500,
+ "session not created": 500,
+ "stale element reference": 404,
+ "timeout": 500,
+ "unable to set cookie": 500,
+ "unable to capture screen": 500,
+ "unexpected alert open": 500,
+ "unknown command": 404,
+ "unknown error": 500,
+ "unknown method": 405,
+ "unsupported operation": 500,
+}
+
+
+def assert_error(response, error_code):
+ """
+ Verify that the provided webdriver.Response instance described
+ a valid error response as defined by `dfn-send-an-error` and
+ the provided error code.
+
+ :param response: ``webdriver.Response`` instance.
+ :param error_code: String value of the expected error code
+ """
+ assert response.status == errors[error_code]
+ assert "value" in response.body
+ assert response.body["value"]["error"] == error_code
+ assert isinstance(response.body["value"]["message"], str)
+ assert isinstance(response.body["value"]["stacktrace"], str)
+ assert_response_headers(response.headers)
+
+
+def assert_success(response, value=None):
+ """
+ Verify that the provided webdriver.Response instance described
+ a valid success response as defined by `dfn-send-a-response` and
+ the provided response value.
+
+ :param response: ``webdriver.Response`` instance.
+ :param value: Expected value of the response body, if any.
+ """
+ assert response.status == 200, str(response.error)
+
+ if value is not None:
+ assert response.body["value"] == value
+
+ assert_response_headers(response.headers)
+ return response.body.get("value")
+
+
+def assert_response_headers(headers):
+ """
+ Method to assert response headers for WebDriver requests
+
+ :param headers: dict with header data
+ """
+ assert 'cache-control' in headers
+ assert 'no-cache' == headers['cache-control']
+ assert 'content-type' in headers
+ assert 'application/json; charset=utf-8' == headers['content-type']
+
+
+def assert_dialog_handled(session, expected_text, expected_retval):
+ # If there were any existing dialogs prior to the creation of this
+ # fixture's dialog, then the "Get Alert Text" command will return
+ # successfully. In that case, the text must be different than that
+ # of this fixture's dialog.
+ try:
+ assert session.alert.text != expected_text, (
+ "User prompt with text '{}' was not handled.".format(expected_text))
+
+ except NoSuchAlertException:
+ # If dialog has been closed and no other one is open, check its return value
+ prompt_retval = session.execute_script(" return window.dialog_return_value;")
+ assert prompt_retval == expected_retval
+
+
+def assert_files_uploaded(session, element, files):
+
+ def get_file_contents(file_index):
+ return session.execute_async_script("""
+ let files = arguments[0].files;
+ let index = arguments[1];
+ let resolve = arguments[2];
+
+ var reader = new FileReader();
+ reader.onload = function(event) {
+ resolve(reader.result);
+ };
+ reader.readAsText(files[index]);
+ """, (element, file_index))
+
+ def get_uploaded_file_names():
+ return session.execute_script("""
+ let fileList = arguments[0].files;
+ let files = [];
+
+ for (var i = 0; i < fileList.length; i++) {
+ files.push(fileList[i].name);
+ }
+
+ return files;
+ """, args=(element,))
+
+ expected_file_names = [str(f.basename) for f in files]
+ assert get_uploaded_file_names() == expected_file_names
+
+ for index, f in enumerate(files):
+ assert get_file_contents(index) == f.read()
+
+
+def assert_is_active_element(session, element):
+ """Verify that element reference is the active element."""
+ from_js = session.execute_script("return document.activeElement")
+
+ if element is None:
+ assert from_js is None
+ else:
+ assert_same_element(session, element, from_js)
+
+
+def assert_same_element(session, a, b):
+ """Verify that two element references describe the same element."""
+ if isinstance(a, dict):
+ assert WebElement.identifier in a, "Actual value does not describe an element"
+ a_id = a[WebElement.identifier]
+ elif isinstance(a, WebElement):
+ a_id = a.id
+ else:
+ raise AssertionError("Actual value is not a dictionary or web element")
+
+ if isinstance(b, dict):
+ assert WebElement.identifier in b, "Expected value does not describe an element"
+ b_id = b[WebElement.identifier]
+ elif isinstance(b, WebElement):
+ b_id = b.id
+ else:
+ raise AssertionError("Expected value is not a dictionary or web element")
+
+ if a_id == b_id:
+ return
+
+ message = ("Expected element references to describe the same element, " +
+ "but they did not.")
+
+ # Attempt to provide more information, accounting for possible errors such
+ # as stale element references or not visible elements.
+ try:
+ a_markup = session.execute_script("return arguments[0].outerHTML;", args=(a,))
+ b_markup = session.execute_script("return arguments[0].outerHTML;", args=(b,))
+ message += " Actual: `%s`. Expected: `%s`." % (a_markup, b_markup)
+ except WebDriverException:
+ pass
+
+ raise AssertionError(message)
+
+
+def assert_in_events(session, expected_events):
+ actual_events = session.execute_script("return window.events")
+ for expected_event in expected_events:
+ assert expected_event in actual_events
+
+
+def assert_events_equal(session, expected_events):
+ actual_events = session.execute_script("return window.events")
+ assert actual_events == expected_events
+
+
+def assert_element_has_focus(target_element):
+ session = target_element.session
+
+ active_element = session.execute_script("return document.activeElement")
+ active_tag = active_element.property("localName")
+ target_tag = target_element.property("localName")
+
+ assert active_element == target_element, (
+ "Focussed element is <%s>, not <%s>" % (active_tag, target_tag))
+
+
+def assert_move_to_coordinates(point, target, events):
+ for e in events:
+ if e["type"] != "mousemove":
+ assert e["pageX"] == point["x"]
+ assert e["pageY"] == point["y"]
+ assert e["target"] == target
+
+
+def assert_pdf(value):
+ data = decodebytes(value.encode())
+
+ assert data.startswith(b"%PDF-"), "Decoded data starts with the PDF signature"
+ assert data.endswith(b"%%EOF\n"), "Decoded data ends with the EOF flag"
+
+
+def assert_png(screenshot):
+ """Test that screenshot is a Base64 encoded PNG file, or a bytestring representing a PNG.
+
+ Returns the bytestring for the PNG, if the assert passes
+ """
+ if type(screenshot) is str:
+ image = decodebytes(screenshot.encode())
+ else:
+ image = screenshot
+ mime_type = imghdr.what("", image)
+ assert mime_type == "png", "Expected image to be PNG, but it was {}".format(mime_type)
+ return image
diff --git a/testing/web-platform/tests/webdriver/tests/support/defaults.py b/testing/web-platform/tests/webdriver/tests/support/defaults.py
new file mode 100644
index 0000000000..64ee18b6c1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/defaults.py
@@ -0,0 +1,6 @@
+SCRIPT_TIMEOUT = 30
+PAGE_LOAD_TIMEOUT = 300
+IMPLICIT_WAIT_TIMEOUT = 0
+
+WINDOW_POSITION = (100, 100)
+WINDOW_SIZE = (800, 600)
diff --git a/testing/web-platform/tests/webdriver/tests/support/fixtures.py b/testing/web-platform/tests/webdriver/tests/support/fixtures.py
new file mode 100644
index 0000000000..7468e8b251
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/fixtures.py
@@ -0,0 +1,489 @@
+import copy
+import json
+import os
+
+import pytest
+import pytest_asyncio
+import webdriver
+
+from urllib.parse import urlunsplit
+
+from tests.support import defaults
+from tests.support.helpers import cleanup_session, deep_update
+from tests.support.inline import build_inline
+from tests.support.http_request import HTTPRequest
+from tests.support.keys import Keys
+
+
+SCRIPT_TIMEOUT = 1
+PAGE_LOAD_TIMEOUT = 3
+IMPLICIT_WAIT_TIMEOUT = 0
+
+# The webdriver session can outlive a pytest session
+_current_session = None
+
+
+def pytest_configure(config):
+ # register the capabilities marker
+ config.addinivalue_line(
+ "markers",
+ "capabilities: mark test to use capabilities"
+ )
+
+
+def pytest_sessionfinish():
+ # Cleanup at the end of a test run
+ global _current_session
+
+ if _current_session is not None:
+ _current_session.end()
+ _current_session = None
+
+
+@pytest.fixture
+def capabilities():
+ """Default capabilities to use for a new WebDriver session."""
+ return {}
+
+
+def pytest_generate_tests(metafunc):
+ if "capabilities" in metafunc.fixturenames:
+ marker = metafunc.definition.get_closest_marker(name="capabilities")
+ if marker:
+ metafunc.parametrize("capabilities", marker.args, ids=None)
+
+
+@pytest.fixture
+def http(configuration):
+ return HTTPRequest(configuration["host"], configuration["port"])
+
+
+@pytest.fixture(scope="session")
+def full_configuration():
+ """Get test configuration information. Keys are:
+
+ host - WebDriver server host.
+ port - WebDriver server port.
+ capabilites - Capabilites passed when creating the WebDriver session
+ timeout_multiplier - Multiplier for timeout values
+ webdriver - Dict with keys `binary`: path to webdriver binary, and
+ `args`: Additional command line arguments passed to the webdriver
+ binary. This doesn't include all the required arguments e.g. the
+ port.
+ wptserve - Configuration of the wptserve servers."""
+
+ with open(os.environ.get("WDSPEC_CONFIG_FILE"), "r") as f:
+ return json.load(f)
+
+
+@pytest.fixture(scope="session")
+def server_config(full_configuration):
+ return full_configuration["wptserve"]
+
+
+@pytest.fixture(scope="session")
+def configuration(full_configuration):
+ """Configuation minus server config.
+
+ This makes logging easier to read."""
+
+ config = full_configuration.copy()
+ del config["wptserve"]
+
+ return config
+
+
+async def reset_current_session_if_necessary(caps):
+ global _current_session
+
+ # If there is a session with different requested capabilities active than
+ # the one we would like to create, end it now.
+ if _current_session is not None:
+ if not _current_session.match(caps):
+ is_bidi = isinstance(_current_session, webdriver.BidiSession)
+ if is_bidi:
+ await _current_session.end()
+ else:
+ _current_session.end()
+ _current_session = None
+
+
+@pytest.fixture()
+def screen_size(session):
+ """Return the size (width/height) of the screen."""
+ return tuple(session.execute_script("""
+ return [
+ screen.width,
+ screen.height,
+ ];
+ """))
+
+
+@pytest.fixture()
+def available_screen_size(session):
+ """Return the effective available screen size (width/height).
+
+ This is size which excludes any fixed window manager elements like menu
+ bars, and the dock on MacOS.
+ """
+ return tuple(session.execute_script("""
+ return [
+ screen.availWidth,
+ screen.availHeight,
+ ];
+ """))
+
+
+@pytest.fixture()
+def minimal_screen_position(session):
+ """Return the minimal position (x/y) a window can be positioned at."""
+ return tuple(session.execute_script("""
+ return [
+ screen.availLeft,
+ screen.availTop,
+ ];
+ """))
+
+
+@pytest_asyncio.fixture(scope="function")
+async def session(capabilities, configuration):
+ """Create and start a session for a test that does not itself test session creation.
+
+ By default the session will stay open after each test, but we always try to start a
+ new one and assume that if that fails there is already a valid session. This makes it
+ possible to recover from some errors that might leave the session in a bad state, but
+ does not demand that we start a new session per test.
+ """
+ global _current_session
+
+ # Update configuration capabilities with custom ones from the
+ # capabilities fixture, which can be set by tests
+ caps = copy.deepcopy(configuration["capabilities"])
+ deep_update(caps, capabilities)
+ caps = {"alwaysMatch": caps}
+
+ await reset_current_session_if_necessary(caps)
+
+ if _current_session is None:
+ _current_session = webdriver.Session(
+ configuration["host"],
+ configuration["port"],
+ capabilities=caps)
+
+ _current_session.start()
+
+ # Enforce a fixed default window size and position
+ if _current_session.capabilities.get("setWindowRect"):
+ # Only resize and reposition if needed to workaround a bug for Chrome:
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+ if _current_session.window.size != defaults.WINDOW_SIZE:
+ _current_session.window.size = defaults.WINDOW_SIZE
+ if _current_session.window.position != defaults.WINDOW_POSITION:
+ _current_session.window.position = defaults.WINDOW_POSITION
+
+ # Set default timeouts
+ multiplier = configuration["timeout_multiplier"]
+ _current_session.timeouts.implicit = IMPLICIT_WAIT_TIMEOUT * multiplier
+ _current_session.timeouts.page_load = PAGE_LOAD_TIMEOUT * multiplier
+ _current_session.timeouts.script = SCRIPT_TIMEOUT * multiplier
+
+ yield _current_session
+
+ cleanup_session(_current_session)
+
+
+@pytest_asyncio.fixture(scope="function")
+async def bidi_session(capabilities, configuration):
+ """Create and start a bidi session.
+
+ Can be used for a test that does not itself test bidi session creation.
+
+ By default the session will stay open after each test, but we always try to start a
+ new one and assume that if that fails there is already a valid session. This makes it
+ possible to recover from some errors that might leave the session in a bad state, but
+ does not demand that we start a new session per test.
+ """
+ global _current_session
+
+ # Update configuration capabilities with custom ones from the
+ # capabilities fixture, which can be set by tests
+ caps = copy.deepcopy(configuration["capabilities"])
+ caps.update({"webSocketUrl": True})
+ deep_update(caps, capabilities)
+ caps = {"alwaysMatch": caps}
+
+ await reset_current_session_if_necessary(caps)
+
+ if _current_session is None:
+ _current_session = webdriver.Session(
+ configuration["host"],
+ configuration["port"],
+ capabilities=caps,
+ enable_bidi=True)
+
+ _current_session.start()
+ await _current_session.bidi_session.start()
+
+ # Enforce a fixed default window size and position
+ if _current_session.capabilities.get("setWindowRect"):
+ # Only resize and reposition if needed to workaround a bug for Chrome:
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+ if _current_session.window.size != defaults.WINDOW_SIZE:
+ _current_session.window.size = defaults.WINDOW_SIZE
+ if _current_session.window.position != defaults.WINDOW_POSITION:
+ _current_session.window.position = defaults.WINDOW_POSITION
+
+ yield _current_session.bidi_session
+
+ await _current_session.bidi_session.end()
+ cleanup_session(_current_session)
+
+
+@pytest.fixture(scope="function")
+def current_session():
+ return _current_session
+
+
+@pytest.fixture
+def url(server_config):
+ def url(path, protocol="https", domain="", subdomain="", query="", fragment=""):
+ domain = server_config["domains"][domain][subdomain]
+ port = server_config["ports"][protocol][0]
+ host = "{0}:{1}".format(domain, port)
+ return urlunsplit((protocol, host, path, query, fragment))
+
+ return url
+
+
+@pytest.fixture
+def modifier_key(current_session):
+ if current_session.capabilities["platformName"] == "mac":
+ return Keys.META
+ else:
+ return Keys.CONTROL
+
+
+@pytest.fixture
+def inline(url):
+ """Take a source extract and produces well-formed documents.
+
+ Based on the desired document type, the extract is embedded with
+ predefined boilerplate in order to produce well-formed documents.
+ The media type and character set may also be individually configured.
+
+ This helper function originally used data URLs, but since these
+ are not universally supported (or indeed standardised!) across
+ browsers, it now delegates the serving of the document to wptserve.
+ This file also acts as a wptserve handler (see the main function
+ below) which configures the HTTP response using query parameters.
+
+ This function returns a URL to the wptserve handler, which in turn
+ will serve an HTTP response with the requested source extract
+ inlined in a well-formed document, and the Content-Type header
+ optionally configured using the desired media type and character set.
+
+ Any additional keyword arguments are passed on to the build_url
+ function, which comes from the url fixture.
+ """
+ def inline(src, **kwargs):
+ return build_inline(url, src, **kwargs)
+
+ return inline
+
+
+@pytest.fixture
+def iframe(inline):
+ """Inline document extract as the source document of an <iframe>."""
+ def iframe(src, **kwargs):
+ return "<iframe src='{}'></iframe>".format(inline(src, **kwargs))
+
+ return iframe
+
+
+@pytest.fixture
+def get_actions_origin_page(inline):
+ """Create a test pagefor action origin tests, recording mouse coordinates
+ automatically on window.coords."""
+
+ def get_actions_origin_page(inner_style, outer_style=""):
+ return inline(
+ f"""
+ <div id="outer" style="{outer_style}"
+ onmousemove="window.coords = {{x: event.clientX, y: event.clientY}}">
+ <div id="inner" style="{inner_style}"></div>
+ </div>
+ """
+ )
+
+ return get_actions_origin_page
+
+
+@pytest.fixture
+def get_test_page(iframe, inline):
+ def get_test_page(
+ as_frame=False,
+ frame_doc=None,
+ shadow_doc=None,
+ nested_shadow_dom=False,
+ shadow_root_mode="open",
+ **kwargs
+ ):
+ if frame_doc is None:
+ frame_doc = """<div id="in-frame"><input type="checkbox"/></div>"""
+
+ if shadow_doc is None:
+ shadow_doc = """<div id="in-shadow-dom"><input type="checkbox"/></div>"""
+
+ definition_inner_shadow_dom = ""
+ if nested_shadow_dom:
+ definition_inner_shadow_dom = f"""
+ customElements.define('inner-custom-element',
+ class extends HTMLElement {{
+ constructor() {{
+ super();
+ this.attachShadow({{mode: "{shadow_root_mode}"}}).innerHTML = `
+ {shadow_doc}
+ `;
+ }}
+ }}
+ );
+ """
+ shadow_doc = """
+ <style>
+ inner-custom-element {
+ display:block; width:20px; height:20px;
+ }
+ </style>
+ <div id="in-nested-shadow-dom">
+ <inner-custom-element></inner-custom-element>
+ </div>
+ """
+
+ page_data = f"""
+ <style>
+ custom-element {{
+ display:block; width:20px; height:20px;
+ }}
+ </style>
+ <div id="with-children"><p><span></span></p><br/></div>
+ <div id="with-text-node">Lorem</div>
+ <div id="with-comment"><!-- Comment --></div>
+
+ <input id="button" type="button"/>
+ <input id="checkbox" type="checkbox"/>
+ <input id="file" type="file"/>
+ <input id="hidden" type="hidden"/>
+ <input id="text" type="text"/>
+
+ {iframe(frame_doc, **kwargs)}
+
+ <img />
+ <svg></svg>
+
+ <custom-element id="custom-element"></custom-element>
+ <script>
+ var svg = document.querySelector("svg");
+ svg.setAttributeNS("http://www.w3.org/2000/svg", "svg:foo", "bar");
+
+ customElements.define("custom-element",
+ class extends HTMLElement {{
+ constructor() {{
+ super();
+ const shadowRoot = this.attachShadow({{mode: "{shadow_root_mode}"}});
+ shadowRoot.innerHTML = `{shadow_doc}`;
+
+ // Save shadow root on window to access it in case of `closed` mode.
+ window._shadowRoot = shadowRoot;
+ }}
+ }}
+ );
+ {definition_inner_shadow_dom}
+ </script>"""
+
+ if as_frame:
+ iframe_data = iframe(page_data, **kwargs)
+ return inline(iframe_data, **kwargs)
+ else:
+ return inline(page_data, **kwargs)
+
+ return get_test_page
+
+
+@pytest.fixture
+def test_origin(url):
+ return url("")
+
+
+@pytest.fixture
+def test_alt_origin(url):
+ return url("", domain="alt")
+
+
+@pytest.fixture
+def test_page(inline):
+ return inline("<div>foo</div>")
+
+
+@pytest.fixture
+def test_page2(inline):
+ return inline("<div>bar</div>")
+
+
+@pytest.fixture
+def test_page_cross_origin(inline):
+ return inline("<div>bar</div>", domain="alt")
+
+
+@pytest.fixture
+def test_page_multiple_frames(inline, test_page, test_page2):
+ return inline(
+ f"<iframe src='{test_page}'></iframe><iframe src='{test_page2}'></iframe>"
+ )
+
+
+@pytest.fixture
+def test_page_nested_frames(inline, test_page_same_origin_frame):
+ return inline(f"<iframe src='{test_page_same_origin_frame}'></iframe>")
+
+
+@pytest.fixture
+def test_page_cross_origin_frame(inline, test_page_cross_origin):
+ return inline(f"<iframe src='{test_page_cross_origin}'></iframe>")
+
+
+@pytest.fixture
+def test_page_same_origin_frame(inline, test_page):
+ return inline(f"<iframe src='{test_page}'></iframe>")
+
+
+@pytest.fixture
+def test_page_with_pdf_js(inline):
+ """Prepare an url to load a PDF document in the browser using pdf.js"""
+ def test_page_with_pdf_js(encoded_pdf_data):
+ return inline("""
+<!doctype html>
+<script src="/_pdf_js/pdf.js"></script>
+<canvas></canvas>
+<script>
+async function getText() {
+ const pages = [];
+ const loadingTask = pdfjsLib.getDocument({data: atob("%s")});
+ const pdf = await loadingTask.promise;
+ for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
+ const page = await pdf.getPage(pageNumber);
+ const textContent = await page.getTextContent();
+ const text = textContent.items.map(x => x.str).join("");
+ pages.push(text);
+ }
+ return pages;
+}
+</script>
+""" % encoded_pdf_data)
+
+ return test_page_with_pdf_js
+
+
+@pytest_asyncio.fixture
+async def top_context(bidi_session):
+ contexts = await bidi_session.browsing_context.get_tree()
+ return contexts[0]
diff --git a/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py b/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py
new file mode 100644
index 0000000000..9566e6ebd1
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py
@@ -0,0 +1,530 @@
+import base64
+
+from tests.support.asserts import assert_pdf
+from tests.support.image import cm_to_px, png_dimensions, ImageDifference
+from typing import Any, Coroutine, Mapping
+
+import asyncio
+import copy
+from datetime import datetime, timedelta
+import pytest
+import pytest_asyncio
+import time
+from webdriver.bidi.error import (
+ InvalidArgumentException,
+ NoSuchFrameException,
+ NoSuchScriptException,
+ NoSuchUserContextException,
+ UnableToSetCookieException,
+ UnderspecifiedStoragePartitionException
+)
+from webdriver.bidi.modules.script import ContextTarget
+from webdriver.error import TimeoutException
+
+
+@pytest_asyncio.fixture
+async def add_preload_script(bidi_session):
+ preload_scripts_ids = []
+
+ async def add_preload_script(function_declaration, arguments=None, contexts=None, sandbox=None):
+ script = await bidi_session.script.add_preload_script(
+ function_declaration=function_declaration,
+ arguments=arguments,
+ contexts=contexts,
+ sandbox=sandbox,
+ )
+ preload_scripts_ids.append(script)
+
+ return script
+
+ yield add_preload_script
+
+ for script in reversed(preload_scripts_ids):
+ try:
+ await bidi_session.script.remove_preload_script(script=script)
+ except (InvalidArgumentException, NoSuchScriptException):
+ pass
+
+
+@pytest_asyncio.fixture
+async def subscribe_events(bidi_session):
+ subscriptions = []
+
+ async def subscribe_events(events, contexts=None):
+ await bidi_session.session.subscribe(events=events, contexts=contexts)
+ subscriptions.append((events, contexts))
+
+ yield subscribe_events
+
+ for events, contexts in reversed(subscriptions):
+ try:
+ await bidi_session.session.unsubscribe(events=events,
+ contexts=contexts)
+ except (InvalidArgumentException, NoSuchFrameException):
+ pass
+
+
+@pytest_asyncio.fixture
+async def set_cookie(bidi_session):
+ """
+ Set a cookie and remove them after the test is finished.
+ """
+ cookies = []
+
+ async def set_cookie(cookie, partition=None):
+ partition_descriptor = None
+ set_cookie_result = await bidi_session.storage.set_cookie(cookie=cookie, partition=partition)
+ if set_cookie_result["partitionKey"] != {}:
+ # Make a copy of the partition key, as the original dict is used for assertion.
+ partition_descriptor = copy.deepcopy(set_cookie_result["partitionKey"])
+ partition_descriptor["type"] = "storageKey"
+ # Store the cookie partition to remove the cookie after the test.
+ # The requested partition can be a browsing context, so the returned partition descriptor (it's always of type
+ # "storageKey") is used.
+ cookies.append((copy.deepcopy(cookie), partition_descriptor))
+ return set_cookie_result
+
+ yield set_cookie
+
+ yesterday = datetime.now() - timedelta(1)
+ yesterday_timestamp = time.mktime(yesterday.timetuple())
+
+ for cookie, partition in reversed(cookies):
+ try:
+ cookie["expiry"] = yesterday_timestamp
+ await bidi_session.storage.set_cookie(cookie=cookie, partition=partition)
+ except (InvalidArgumentException, UnableToSetCookieException, UnderspecifiedStoragePartitionException):
+ pass
+
+
+@pytest_asyncio.fixture
+async def new_tab(bidi_session):
+ """Open and focus a new tab to run the test in a foreground tab."""
+ new_tab = await bidi_session.browsing_context.create(type_hint='tab')
+
+ yield new_tab
+
+ try:
+ await bidi_session.browsing_context.close(context=new_tab["context"])
+ except NoSuchFrameException:
+ print(f"Tab with id {new_tab['context']} has already been closed")
+
+
+@pytest.fixture
+def send_blocking_command(bidi_session):
+ """Send a blocking command that awaits until the BiDi response has been received."""
+ async def send_blocking_command(command: str, params: Mapping[str, Any]) -> Mapping[str, Any]:
+ future_response = await bidi_session.send_command(command, params)
+ return await future_response
+ return send_blocking_command
+
+
+@pytest.fixture
+def wait_for_event(bidi_session, event_loop):
+ """Wait until the BiDi session emits an event and resolve the event data."""
+ remove_listeners = []
+
+ def wait_for_event(event_name: str):
+ future = event_loop.create_future()
+
+ async def on_event(_, data):
+ remove_listener()
+ remove_listeners.remove(remove_listener)
+ future.set_result(data)
+
+ remove_listener = bidi_session.add_event_listener(event_name, on_event)
+ remove_listeners.append(remove_listener)
+ return future
+
+ yield wait_for_event
+
+ # Cleanup any leftover callback for which no event was captured.
+ for remove_listener in remove_listeners:
+ remove_listener()
+
+
+@pytest.fixture
+def wait_for_future_safe(configuration):
+ """Wait for the given future for a given amount of time.
+ Fails gracefully if the future does not resolve within the given timeout."""
+
+ async def wait_for_future_safe(future: Coroutine, timeout: float = 2.0):
+ try:
+ return await asyncio.wait_for(
+ asyncio.shield(future),
+ timeout=timeout * configuration["timeout_multiplier"],
+ )
+ except asyncio.exceptions.TimeoutError:
+ raise TimeoutException("Future did not resolve within the given timeout")
+
+ return wait_for_future_safe
+
+
+@pytest.fixture
+def current_time(bidi_session, top_context):
+ """Get the current time stamp in ms from the remote end.
+
+ This is required especially when tests are run on different devices like
+ for Android, where it's not guaranteed that both machines are in sync.
+ """
+ async def current_time():
+ result = await bidi_session.script.evaluate(
+ expression="Date.now()",
+ target=ContextTarget(top_context["context"]),
+ await_promise=True)
+ return result["value"]
+
+ return current_time
+
+
+@pytest.fixture
+def add_and_remove_iframe(bidi_session):
+ """Create a frame, wait for load, and remove it.
+
+ Return the frame's context id, which allows to test for invalid
+ browsing context references.
+ """
+
+ async def closed_frame(context):
+ initial_contexts = await bidi_session.browsing_context.get_tree(root=context["context"])
+ resp = await bidi_session.script.call_function(
+ function_declaration="""(url) => {
+ const iframe = document.createElement("iframe");
+ // Once we're confident implementations support returning the iframe, just
+ // return that directly. For now generate a unique id to use as a handle.
+ const id = `testframe-${Math.random()}`;
+ iframe.id = id;
+ iframe.src = url;
+ document.documentElement.lastElementChild.append(iframe);
+ return new Promise(resolve => iframe.onload = () => resolve(id));
+ }""",
+ target={"context": context["context"]},
+ await_promise=True)
+ iframe_dom_id = resp["value"]
+
+ new_contexts = await bidi_session.browsing_context.get_tree(root=context["context"])
+ added_contexts = ({item["context"] for item in new_contexts[0]["children"]} -
+ {item["context"] for item in initial_contexts[0]["children"]})
+ assert len(added_contexts) == 1
+ frame_id = added_contexts.pop()
+
+ await bidi_session.script.evaluate(
+ expression=f"document.getElementById('{iframe_dom_id}').remove()",
+ target={"context": context["context"]},
+ await_promise=False)
+
+ return frame_id
+ return closed_frame
+
+
+@pytest.fixture
+def load_pdf_bidi(bidi_session, test_page_with_pdf_js, top_context):
+ """Load a PDF document in the browser using pdf.js"""
+ async def load_pdf_bidi(encoded_pdf_data, context=top_context["context"]):
+ url = test_page_with_pdf_js(encoded_pdf_data)
+
+ await bidi_session.browsing_context.navigate(
+ context=context, url=url, wait="complete"
+ )
+
+ return load_pdf_bidi
+
+
+@pytest.fixture
+def get_pdf_content(bidi_session, top_context, load_pdf_bidi):
+ """Load a PDF document in the browser using pdf.js and extract content from the document"""
+ async def get_pdf_content(encoded_pdf_data, context=top_context["context"]):
+ await load_pdf_bidi(encoded_pdf_data=encoded_pdf_data, context=context)
+
+ result = await bidi_session.script.call_function(
+ function_declaration="() => { return window.getText(); }",
+ target=ContextTarget(context),
+ await_promise=True,
+ )
+
+ return result
+
+ return get_pdf_content
+
+
+@pytest.fixture
+def assert_pdf_content(new_tab, get_pdf_content):
+ """Assert PDF with provided content"""
+ async def assert_pdf_content(pdf, expected_content):
+ assert_pdf(pdf)
+
+ pdf_content = await get_pdf_content(pdf, new_tab["context"])
+
+ assert pdf_content == {
+ "type": "array",
+ "value": expected_content,
+ }
+
+ return assert_pdf_content
+
+
+@pytest.fixture
+def assert_pdf_dimensions(render_pdf_to_png_bidi):
+ """Assert PDF dimensions"""
+ async def assert_pdf_dimensions(pdf, expected_dimensions):
+ assert_pdf(pdf)
+
+ png = await render_pdf_to_png_bidi(pdf)
+ width, height = png_dimensions(png)
+
+ # account for potential rounding errors
+ assert (height - 1) <= cm_to_px(expected_dimensions["height"]) <= (height + 1)
+ assert (width - 1) <= cm_to_px(expected_dimensions["width"]) <= (width + 1)
+
+ return assert_pdf_dimensions
+
+
+@pytest.fixture
+def assert_pdf_image(
+ get_reference_png, render_pdf_to_png_bidi, compare_png_bidi
+):
+ """Assert PDF with image generated for provided html"""
+ async def assert_pdf_image(pdf, reference_html, expected):
+ assert_pdf(pdf)
+
+ reference_png = await get_reference_png(reference_html)
+ page_without_background_png = await render_pdf_to_png_bidi(pdf)
+ comparison_without_background = await compare_png_bidi(
+ reference_png,
+ page_without_background_png,
+ )
+
+ assert comparison_without_background.equal() == expected
+
+ return assert_pdf_image
+
+
+@pytest.fixture
+def compare_png_bidi(bidi_session, url):
+ async def compare_png_bidi(img1, img2):
+ """Calculate difference statistics between two PNG images.
+
+ :param img1: Bytes of first PNG image
+ :param img2: Bytes of second PNG image
+ :returns: ImageDifference representing the total number of different pixels,
+ and maximum per-channel difference between the images.
+ """
+ if img1 == img2:
+ return ImageDifference(0, 0)
+
+ width, height = png_dimensions(img1)
+ assert (width, height) == png_dimensions(img2)
+
+ context = await bidi_session.browsing_context.create(type_hint="tab")
+ await bidi_session.browsing_context.navigate(
+ context=context["context"],
+ url=url("/webdriver/tests/support/html/render.html"),
+ wait="complete",
+ )
+ result = await bidi_session.script.call_function(
+ function_declaration="""(img1, img2, width, height) => {
+ return compare(img1, img2, width, height)
+ }""",
+ target=ContextTarget(context["context"]),
+ arguments=[
+ {"type": "string", "value": base64.encodebytes(img1).decode()},
+ {"type": "string", "value": base64.encodebytes(img2).decode()},
+ {"type": "number", "value": width},
+ {"type": "number", "value": height},
+ ],
+ await_promise=True,
+ )
+ await bidi_session.browsing_context.close(context=context["context"])
+ assert result["type"] == "object"
+ assert set(item[0] for item in result["value"]) == {"totalPixels", "maxDifference"}
+ for item in result["value"]:
+ assert len(item) == 2
+ assert item[1]["type"] == "number"
+ if item[0] == "totalPixels":
+ total_pixels = item[1]["value"]
+ elif item[0] == "maxDifference":
+ max_difference = item[1]["value"]
+ else:
+ raise Exception(f"Unexpected object key ${item[0]}")
+ return ImageDifference(total_pixels, max_difference)
+ return compare_png_bidi
+
+
+@pytest.fixture
+def current_url(bidi_session):
+ async def current_url(context):
+ contexts = await bidi_session.browsing_context.get_tree(root=context, max_depth=0)
+ return contexts[0]["url"]
+
+ return current_url
+
+
+@pytest.fixture
+def get_element(bidi_session, top_context):
+ async def get_element(css_selector, context=top_context):
+ result = await bidi_session.script.evaluate(
+ expression=f"document.querySelector('{css_selector}')",
+ target=ContextTarget(context["context"]),
+ await_promise=False,
+ )
+ return result
+ return get_element
+
+
+@pytest.fixture
+def get_reference_png(
+ bidi_session, inline, render_pdf_to_png_bidi, top_context
+):
+ """Print to PDF provided content and render it to png"""
+ async def get_reference_png(reference_content, context=top_context["context"]):
+ reference_page = inline(reference_content)
+ await bidi_session.browsing_context.navigate(
+ context=context, url=reference_page, wait="complete"
+ )
+
+ reference_pdf = await bidi_session.browsing_context.print(
+ context=context,
+ background=True,
+ )
+
+ return await render_pdf_to_png_bidi(reference_pdf)
+
+ return get_reference_png
+
+
+@pytest.fixture
+def render_pdf_to_png_bidi(bidi_session, new_tab, url):
+ """Render a PDF document to png"""
+
+ async def render_pdf_to_png_bidi(
+ encoded_pdf_data, page=1
+ ):
+ await bidi_session.browsing_context.navigate(
+ context=new_tab["context"],
+ url=url(path="/print_pdf_runner.html"),
+ wait="complete",
+ )
+
+ result = await bidi_session.script.call_function(
+ function_declaration=f"""() => {{ return window.render("{encoded_pdf_data}"); }}""",
+ target=ContextTarget(new_tab["context"]),
+ await_promise=True,
+ )
+ value = result["value"]
+ index = page - 1
+
+ assert 0 <= index < len(value)
+
+ image_string = value[index]["value"]
+ image_string_without_data_type = image_string[image_string.find(",") +
+ 1:]
+
+ return base64.b64decode(image_string_without_data_type)
+
+ return render_pdf_to_png_bidi
+
+
+@pytest.fixture
+def load_static_test_page(bidi_session, url, top_context):
+ """Navigate to a test page from the support/html folder."""
+
+ async def load_static_test_page(page, context=top_context):
+ await bidi_session.browsing_context.navigate(
+ context=context["context"],
+ url=url(f"/webdriver/tests/support/html/{page}"),
+ wait="complete",
+ )
+
+ return load_static_test_page
+
+
+@pytest_asyncio.fixture
+async def create_user_context(bidi_session):
+ """Create a user context and ensure it is removed at the end of the test."""
+
+ user_contexts = []
+
+ async def create_user_context():
+ nonlocal user_contexts
+ user_context = await bidi_session.browser.create_user_context()
+ user_contexts.append(user_context)
+
+ return user_context
+
+ yield create_user_context
+
+ # Remove all created user contexts at the end of the test
+ for user_context in user_contexts:
+ try:
+ await bidi_session.browser.remove_user_context(user_context=user_context)
+ except NoSuchUserContextException:
+ # Ignore exceptions in case a specific user context was already
+ # removed during the test.
+ pass
+
+
+@pytest_asyncio.fixture
+async def add_cookie(bidi_session):
+ """
+ Add a cookie with `document.cookie` and remove them after the test is finished.
+ """
+ cookies = []
+
+ async def add_cookie(
+ context,
+ name,
+ value,
+ domain=None,
+ expiry=None,
+ path=None,
+ same_site="none",
+ secure=False,
+ ):
+ cookie_string = f"{name}={value}"
+ cookie = {"name": name, "context": context}
+
+ if domain is not None:
+ cookie_string += f";domain={domain}"
+
+ if expiry is not None:
+ cookie_string += f";expires={expiry}"
+
+ if path is not None:
+ cookie_string += f";path={path}"
+ cookie["path"] = path
+
+ if same_site != "none":
+ cookie_string += f";SameSite={same_site}"
+
+ if secure is True:
+ cookie_string += ";Secure"
+
+ await bidi_session.script.evaluate(
+ expression=f"document.cookie = '{cookie_string}'",
+ target=ContextTarget(context),
+ await_promise=True,
+ )
+
+ cookies.append(cookie)
+
+ yield add_cookie
+
+ for cookie in reversed(cookies):
+ cookie_string = f"""{cookie["name"]}="""
+
+ if "path" in cookie:
+ cookie_string += f""";path={cookie["path"]}"""
+
+ await bidi_session.script.evaluate(
+ expression=f"""document.cookie = '{cookie_string};Max-Age=0'""",
+ target=ContextTarget(cookie["context"]),
+ await_promise=True,
+ )
+
+
+@pytest.fixture
+def domain_value(server_config):
+ def domain_value(domain="", subdomain=""):
+ return server_config["domains"][domain][subdomain]
+
+ return domain_value
diff --git a/testing/web-platform/tests/webdriver/tests/support/fixtures_http.py b/testing/web-platform/tests/webdriver/tests/support/fixtures_http.py
new file mode 100644
index 0000000000..dd714f5e7c
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/fixtures_http.py
@@ -0,0 +1,240 @@
+import base64
+
+import pytest
+from webdriver.error import NoSuchAlertException
+
+from tests.support.image import png_dimensions, ImageDifference
+from tests.support.sync import Poll
+
+
+@pytest.fixture
+def add_event_listeners():
+ """Register listeners for tracked events on element."""
+ def add_event_listeners(element, tracked_events):
+ element.session.execute_script("""
+ const element = arguments[0];
+ const trackedEvents = arguments[1];
+
+ if (!("events" in window)) {
+ window.events = [];
+ }
+
+ for (let i = 0; i < trackedEvents.length; i++) {
+ element.addEventListener(trackedEvents[i], function (event) {
+ window.events.push(event.type);
+ });
+ }
+ """, args=(element, tracked_events))
+ return add_event_listeners
+
+
+@pytest.fixture
+def closed_frame(session, url):
+ """Create a frame and remove it after switching to it.
+
+ The removed frame will be kept selected, which allows to test for invalid
+ browsing context references.
+ """
+ original_handle = session.window_handle
+ new_handle = session.new_window()
+
+ session.window_handle = new_handle
+
+ session.url = url("/webdriver/tests/support/html/frames.html")
+
+ subframe = session.find.css("#sub-frame", all=False)
+ session.switch_frame(subframe)
+
+ deleteframe = session.find.css("#delete-frame", all=False)
+ session.switch_frame(deleteframe)
+
+ button = session.find.css("#remove-parent", all=False)
+ button.click()
+
+ yield
+
+ session.window.close()
+ assert new_handle not in session.handles, "Unable to close window {}".format(new_handle)
+
+ session.window_handle = original_handle
+
+
+@pytest.fixture
+def closed_window(session, inline):
+ """Create a window and close it immediately.
+
+ The window handle will be kept selected, which allows to test for invalid
+ top-level browsing context references.
+ """
+ original_handle = session.window_handle
+ new_handle = session.new_window()
+
+ session.window_handle = new_handle
+ session.url = inline("<input id='a' value='b'>")
+ element = session.find.css("input", all=False)
+
+ session.window.close()
+ assert new_handle not in session.handles, "Unable to close window {}".format(new_handle)
+
+ yield (original_handle, element)
+
+ session.window_handle = original_handle
+
+
+@pytest.fixture
+def create_cookie(session, url):
+ """Create a cookie."""
+ def create_cookie(name, value, **kwargs):
+ if kwargs.get("path", None) is not None:
+ session.url = url(kwargs["path"])
+
+ session.set_cookie(name, value, **kwargs)
+ return session.cookies(name)
+
+ return create_cookie
+
+
+@pytest.fixture
+def create_dialog(session):
+ """Create a dialog (one of "alert", "prompt", or "confirm").
+
+ Also it provides a function to validate that the dialog has been "handled"
+ (either accepted or dismissed) by returning some value.
+ """
+ def create_dialog(dialog_type, text=None):
+ assert dialog_type in ("alert", "confirm", "prompt"), (
+ "Invalid dialog type: '%s'" % dialog_type)
+
+ if text is None:
+ text = ""
+
+ assert isinstance(text, str), "`text` parameter must be a string"
+
+ # Script completes itself when the user prompt has been opened.
+ # For prompt() dialogs, add a value for the 'default' argument,
+ # as some user agents (IE, for example) do not produce consistent
+ # values for the default.
+ session.execute_async_script("""
+ let dialog_type = arguments[0];
+ let text = arguments[1];
+
+ setTimeout(function() {
+ if (dialog_type == 'prompt') {
+ window.dialog_return_value = window[dialog_type](text, '');
+ } else {
+ window.dialog_return_value = window[dialog_type](text);
+ }
+ }, 0);
+ """, args=(dialog_type, text))
+
+ wait = Poll(
+ session,
+ timeout=15,
+ ignored_exceptions=NoSuchAlertException,
+ message="No user prompt with text '{}' detected".format(text))
+ wait.until(lambda s: s.alert.text == text)
+
+ return create_dialog
+
+
+@pytest.fixture
+def create_frame(session):
+ """Create an `iframe` element.
+
+ The element will be inserted into the document of the current browsing
+ context. Return a reference to the newly-created element.
+ """
+ def create_frame():
+ append = """
+ var frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ return frame;
+ """
+ return session.execute_script(append)
+
+ return create_frame
+
+
+@pytest.fixture
+def stale_element(current_session, get_test_page):
+ """Create a stale element reference
+
+ The document will be loaded in the top-level or child browsing context.
+ Before the requested element or its shadow root is returned the element
+ is removed from the document's DOM.
+ """
+ def stale_element(css_value, as_frame=False, want_shadow_root=False):
+ current_session.url = get_test_page(as_frame=as_frame)
+
+ if as_frame:
+ frame = current_session.find.css("iframe", all=False)
+ current_session.switch_frame(frame)
+
+ element = current_session.find.css(css_value, all=False)
+ shadow_root = element.shadow_root if want_shadow_root else None
+
+ current_session.execute_script("arguments[0].remove();", args=[element])
+
+ return shadow_root if want_shadow_root else element
+
+ return stale_element
+
+
+@pytest.fixture
+def load_pdf_http(current_session, test_page_with_pdf_js):
+ """Load a PDF document in the browser using pdf.js"""
+ def load_pdf_http(encoded_pdf_data):
+ current_session.url = test_page_with_pdf_js(encoded_pdf_data)
+
+ return load_pdf_http
+
+
+@pytest.fixture
+def render_pdf_to_png_http(current_session, url):
+ """Render a PDF document to png"""
+
+ def render_pdf_to_png_http(
+ encoded_pdf_data, page=1
+ ):
+ current_session.url = url(path="/print_pdf_runner.html")
+ result = current_session.execute_async_script(f"""arguments[0](window.render("{encoded_pdf_data}"))""")
+ index = page - 1
+
+ assert 0 <= index < len(result)
+
+ image_string = result[index]
+ image_string_without_data_type = image_string[image_string.find(",") + 1:]
+
+ return base64.b64decode(image_string_without_data_type)
+
+ return render_pdf_to_png_http
+
+
+@pytest.fixture
+def compare_png_http(current_session, url):
+ def compare_png_http(img1, img2):
+ """Calculate difference statistics between two PNG images.
+
+ :param img1: Bytes of first PNG image
+ :param img2: Bytes of second PNG image
+ :returns: ImageDifference representing the total number of different pixels,
+ and maximum per-channel difference between the images.
+ """
+ if img1 == img2:
+ return ImageDifference(0, 0)
+
+ width, height = png_dimensions(img1)
+ assert (width, height) == png_dimensions(img2)
+
+ current_session.url = url("/webdriver/tests/support/html/render.html")
+ result = current_session.execute_async_script(
+ "const callback = arguments[arguments.length - 1]; callback(compare(arguments[0], arguments[1], arguments[2], arguments[3]))",
+ args=[base64.encodebytes(img1).decode(), base64.encodebytes(img2).decode(), width, height],
+ )
+
+ assert "maxDifference" in result
+ assert "totalPixels" in result
+
+ return ImageDifference(result["totalPixels"], result["maxDifference"])
+
+ return compare_png_http
diff --git a/testing/web-platform/tests/webdriver/tests/support/helpers.py b/testing/web-platform/tests/webdriver/tests/support/helpers.py
new file mode 100644
index 0000000000..678733e951
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/helpers.py
@@ -0,0 +1,273 @@
+import collections
+import math
+import sys
+from urllib.parse import urlparse
+
+import webdriver
+
+from tests.support import defaults
+from tests.support.sync import Poll
+
+
+def ignore_exceptions(f):
+ def inner(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except webdriver.error.WebDriverException as e:
+ print("Ignored exception %s" % e, file=sys.stderr)
+ inner.__name__ = f.__name__
+ return inner
+
+
+def cleanup_session(session):
+ """Clean-up the current session for a clean state."""
+ @ignore_exceptions
+ def _dismiss_user_prompts(session):
+ """Dismiss any open user prompts in windows."""
+ current_window = session.window_handle
+
+ for window in _windows(session):
+ session.window_handle = window
+ try:
+ session.alert.dismiss()
+ except webdriver.NoSuchAlertException:
+ pass
+
+ session.window_handle = current_window
+
+ @ignore_exceptions
+ def _ensure_valid_window(session):
+ """If current window was closed, ensure to have a valid one selected."""
+ try:
+ session.window_handle
+ except webdriver.NoSuchWindowException:
+ session.window_handle = session.handles[0]
+
+ @ignore_exceptions
+ def _restore_timeouts(session):
+ """Restore modified timeouts to their default values."""
+ session.timeouts.implicit = defaults.IMPLICIT_WAIT_TIMEOUT
+ session.timeouts.page_load = defaults.PAGE_LOAD_TIMEOUT
+ session.timeouts.script = defaults.SCRIPT_TIMEOUT
+
+ @ignore_exceptions
+ def _restore_window_state(session):
+ """Reset window to an acceptable size.
+
+ This also includes bringing it out of maximized, minimized,
+ or fullscreened state.
+ """
+ if session.capabilities.get("setWindowRect"):
+ # Only restore if needed to workaround a bug for Chrome:
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=4642#c4
+ if (
+ session.capabilities.get("browserName") != "chrome" or
+ session.window.size != defaults.WINDOW_SIZE
+ or document_hidden(session)
+ or is_fullscreen(session)
+ or is_maximized(session)
+ ):
+ session.window.size = defaults.WINDOW_SIZE
+
+ @ignore_exceptions
+ def _restore_windows(session):
+ """Close superfluous windows opened by the test.
+
+ It will not end the session implicitly by closing the last window.
+ """
+ current_window = session.window_handle
+
+ for window in _windows(session, exclude=[current_window]):
+ session.window_handle = window
+ if len(session.handles) > 1:
+ session.window.close()
+
+ session.window_handle = current_window
+
+ _restore_timeouts(session)
+ _ensure_valid_window(session)
+ _dismiss_user_prompts(session)
+ _restore_windows(session)
+ _restore_window_state(session)
+ _switch_to_top_level_browsing_context(session)
+
+
+@ignore_exceptions
+def _switch_to_top_level_browsing_context(session):
+ """If the current browsing context selected by WebDriver is a
+ `<frame>` or an `<iframe>`, switch it back to the top-level
+ browsing context.
+ """
+ session.switch_frame(None)
+
+
+def _windows(session, exclude=None):
+ """Set of window handles, filtered by an `exclude` list if
+ provided.
+ """
+ if exclude is None:
+ exclude = []
+ wins = [w for w in session.handles if w not in exclude]
+ return set(wins)
+
+
+def clear_all_cookies(session):
+ """Removes all cookies associated with the current active document"""
+ session.transport.send("DELETE", "session/%s/cookie" % session.session_id)
+
+
+def deep_update(source, overrides):
+ """
+ Update a nested dictionary or similar mapping.
+ Modify ``source`` in place.
+ """
+ for key, value in overrides.items():
+ if isinstance(value, collections.abc.Mapping) and value:
+ returned = deep_update(source.get(key, {}), value)
+ source[key] = returned
+ else:
+ source[key] = overrides[key]
+ return source
+
+
+def document_dimensions(session):
+ return tuple(session.execute_script("""
+ const {devicePixelRatio} = window;
+ const {width, height} = document.documentElement.getBoundingClientRect();
+ return [width * devicePixelRatio, height * devicePixelRatio];
+ """))
+
+
+def center_point(element):
+ """Calculates the in-view center point of a web element."""
+ inner_width, inner_height = element.session.execute_script(
+ "return [window.innerWidth, window.innerHeight]")
+ rect = element.rect
+
+ # calculate the intersection of the rect that is inside the viewport
+ visible = {
+ "left": max(0, min(rect["x"], rect["x"] + rect["width"])),
+ "right": min(inner_width, max(rect["x"], rect["x"] + rect["width"])),
+ "top": max(0, min(rect["y"], rect["y"] + rect["height"])),
+ "bottom": min(inner_height, max(rect["y"], rect["y"] + rect["height"])),
+ }
+
+ # arrive at the centre point of the visible rectangle
+ x = (visible["left"] + visible["right"]) / 2.0
+ y = (visible["top"] + visible["bottom"]) / 2.0
+
+ # convert to CSS pixels, as centre point can be float
+ return (math.floor(x), math.floor(y))
+
+
+def document_hidden(session):
+ return session.execute_script("return document.hidden")
+
+
+def document_location(session):
+ """
+ Unlike ``webdriver.Session#url``, which always returns
+ the top-level browsing context's URL, this returns
+ the current browsing context's active document's URL.
+ """
+ return session.execute_script("return document.location.href")
+
+
+def element_rect(session, element):
+ return session.execute_script("""
+ let element = arguments[0];
+ let rect = element.getBoundingClientRect();
+
+ return {
+ x: rect.left + window.pageXOffset,
+ y: rect.top + window.pageYOffset,
+ width: rect.width,
+ height: rect.height,
+ };
+ """, args=(element,))
+
+
+def is_element_in_viewport(session, element):
+ """Check if element is outside of the viewport"""
+ return session.execute_script("""
+ let el = arguments[0];
+
+ let rect = el.getBoundingClientRect();
+ let viewport = {
+ height: window.innerHeight || document.documentElement.clientHeight,
+ width: window.innerWidth || document.documentElement.clientWidth,
+ };
+
+ return !(rect.right < 0 || rect.bottom < 0 ||
+ rect.left > viewport.width || rect.top > viewport.height)
+ """, args=(element,))
+
+
+def is_fullscreen(session):
+ # At the time of writing, WebKit does not conform to the
+ # Fullscreen API specification.
+ #
+ # Remove the prefixed fallback when
+ # https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed.
+ return session.execute_script("""
+ return !!(window.fullScreen || document.webkitIsFullScreen)
+ """)
+
+
+def is_maximized(session):
+ dimensions = session.execute_script("""
+ return {
+ availWidth: screen.availWidth,
+ availHeight: screen.availHeight,
+ windowWidth: window.outerWidth,
+ windowHeight: window.outerHeight,
+ }
+ """)
+
+ return (
+ # The maximized window can still have a border attached which would
+ # cause its dimensions to exceed the whole available screen.
+ dimensions["windowWidth"] >= dimensions["availWidth"] and
+ dimensions["windowHeight"] >= dimensions["availHeight"] and
+ # Only return true if the window is not in fullscreen mode
+ not is_fullscreen(session)
+ )
+
+
+def filter_dict(source, d):
+ """Filter `source` dict to only contain same keys as `d` dict.
+
+ :param source: dictionary to filter.
+ :param d: dictionary whose keys determine the filtering.
+ """
+ return {k: source[k] for k in d.keys()}
+
+
+def filter_supported_key_events(all_events, expected):
+ events = [filter_dict(e, expected[0]) for e in all_events]
+ if len(events) > 0 and events[0]["code"] is None:
+ # Remove 'code' entry if browser doesn't support it
+ expected = [filter_dict(e, {"key": "", "type": ""}) for e in expected]
+ events = [filter_dict(e, expected[0]) for e in events]
+
+ return (events, expected)
+
+
+def get_origin_from_url(url):
+ parsed_uri = urlparse(url)
+ return '{uri.scheme}://{uri.netloc}'.format(uri=parsed_uri)
+
+
+def wait_for_new_handle(session, handles_before):
+ def find_new_handle(session):
+ new_handles = list(set(session.handles) - set(handles_before))
+ if new_handles and len(new_handles) == 1:
+ return new_handles[0]
+ return None
+
+ wait = Poll(
+ session,
+ timeout=5,
+ message="No new window has been opened")
+
+ return wait.until(find_new_handle)
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/beforeunload.html b/testing/web-platform/tests/webdriver/tests/support/html/beforeunload.html
new file mode 100644
index 0000000000..d4332c2894
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/beforeunload.html
@@ -0,0 +1,16 @@
+<html>
+
+ <head>
+ <script>
+ window.addEventListener("beforeunload", function (event) {
+ event.preventDefault();
+ });
+ </script>
+ </head>
+
+ <body>
+ <input type="text" />
+ <a href="default.html" target="_top">Click</a>
+ </body>
+
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/default.html b/testing/web-platform/tests/webdriver/tests/support/html/default.html
new file mode 100644
index 0000000000..c15d0a7eb7
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/default.html
@@ -0,0 +1,7 @@
+<html>
+
+ <body>
+ <div>Foo</div>
+ </body>
+
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/deleteframe.html b/testing/web-platform/tests/webdriver/tests/support/html/deleteframe.html
new file mode 100644
index 0000000000..fd757e6db0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/deleteframe.html
@@ -0,0 +1,6 @@
+<html>
+<body>
+ <input type="button" id="remove-parent" onclick="parent.remove();" value="Remove parent frame" />
+ <input type="button" id="remove-top" onclick="top.remove();" value="Remove top frame" />
+</body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/frames.html b/testing/web-platform/tests/webdriver/tests/support/html/frames.html
new file mode 100644
index 0000000000..81c6f9b383
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/frames.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <script type="text/javascript">
+ function remove() {
+ const frame = document.getElementById("sub-frame");
+ const div = document.getElementById("delete");
+ div.removeChild(frame);
+ }
+ </script>
+</head>
+<body>
+ <div id="delete">
+ <iframe src="subframe.html" id="sub-frame"></iframe>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/frames_no_bfcache.html b/testing/web-platform/tests/webdriver/tests/support/html/frames_no_bfcache.html
new file mode 100644
index 0000000000..1972187d21
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/frames_no_bfcache.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+ <script type="text/javascript">
+ function remove() {
+ const frame = document.getElementById("sub-frame");
+ const div = document.getElementById("delete");
+ div.removeChild(frame);
+ }
+ </script>
+</head>
+
+<!-- unload handler prevents the page from being added to the bfcache on navigation -->
+<body onunload="">
+ <div id="delete">
+ <iframe src="subframe.html" id="sub-frame"></iframe>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/meta-utf8-after-1024-bytes.html b/testing/web-platform/tests/webdriver/tests/support/html/meta-utf8-after-1024-bytes.html
new file mode 100644
index 0000000000..b5916148b5
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/meta-utf8-after-1024-bytes.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla eu iaculis
+lectus. Quisque ullamcorper est at nunc consectetur suscipit. Aliquam imperdiet
+mauris in nulla ornare, id eleifend turpis placerat. Vestibulum lorem libero,
+sollicitudin in orci suscipit, dictum vestibulum nulla. Ut ac est tincidunt,
+cursus leo vel, pellentesque orci. Sed mattis metus augue, ac tincidunt nunc
+lobortis in. Proin eu ipsum auctor lorem sagittis malesuada. Vivamus maximus,
+eros fringilla vulputate tincidunt, tellus tellus viverra augue, sed iaculis
+ipsum lacus quis tellus. Morbi et enim at ante molestie imperdiet et et nulla.
+Aliquam consequat rhoncus magna, vitae sodales urna maximus eget. Mauris eu
+laoreet turpis, eget condimentum lectus. Maecenas vel lorem vel nulla efficitur
+euismod. Sed lobortis enim ac odio bibendum, id vehicula nibh tempus. Phasellus
+sodales, ipsum feugiat aliquam vehicula, diam leo cursus est, nec varius nunc
+felis vitae est. Curabitur ac purus nisl. Mauris condimentum, magna quis
+consectetur biam. -->
+<meta charset="utf-8">
+<div id="body"></div>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/render.html b/testing/web-platform/tests/webdriver/tests/support/html/render.html
new file mode 100644
index 0000000000..6f1fadb64b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/render.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<canvas></canvas>
+<script>
+async function render(ctx, imgBase64, width, height) {
+ ctx.clearRect(0, 0, width, height);
+ const img = new Image();
+ const loaded = new Promise(resolve => img.addEventListener("load" , resolve, false));
+ img.src = `data:image/png;base64,${imgBase64}`;
+ await loaded;
+ ctx.drawImage(img, 0, 0);
+ return ctx.getImageData(0, 0, width, height);
+}
+
+function compareImgData(img1, img2) {
+ if (img1.width !== img2.width) {
+ throw new Error(`Image widths don't match; got ${img1.width} and ${img2.width}`)
+ }
+ if (img1.height !== img2.height) {
+ throw new Error(`Image heights don't match; got ${img1.height} and ${img2.height}`)
+ }
+
+ const result = {totalPixels: 0, maxDifference: 0};
+
+ const img1Data = img1.data;
+ const img2Data = img2.data;
+
+ let idx = 0;
+ while (idx < img1Data.length) {
+ let maxDifference = 0;
+ for (let channel = 0; channel < 4; channel++) {
+ const difference = Math.abs(img1Data[idx + channel] - img2Data[idx + channel]);
+ if (difference > maxDifference) {
+ maxDifference = difference
+ }
+ }
+ if (maxDifference > 0) {
+ result.totalPixels += 1;
+ if (maxDifference > result.maxDifference) {
+ result.maxDifference = maxDifference;
+ }
+ }
+ idx += 4;
+ }
+ return result;
+}
+
+/**
+ * Compare two images for equality.
+ *
+ * @param {string} img1 - base64-encoded string of image 1
+ * @param {string} img2 - base64-encoded string of image 2
+ * @param {number} width - Image width in pixels
+ * @param {number} height - Image height in pixels
+ * @returns {Promise<Object>} - A promise that resolves to an object containing `totalPixels`; the
+ * number of pixels different between the images, and `maxDifference`
+ * the maximum difference in any color channel.
+ */
+async function compare(img1, img2, width, height) {
+ const canvas = document.getElementsByTagName("canvas")[0];
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext("2d");
+
+ let img1Data = await render(ctx, img1, width, height);
+ let img2Data = await render(ctx, img2, width, height);
+ return compareImgData(img1Data, img2Data, width, height);
+}
+</script>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/subframe.html b/testing/web-platform/tests/webdriver/tests/support/html/subframe.html
new file mode 100644
index 0000000000..2019485529
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/subframe.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <script type="text/javascript">
+ function remove() {
+ const frame = document.getElementById("delete-frame");
+ const div = document.getElementById("delete");
+ div.removeChild(frame);
+ }
+ </script>
+</head>
+<body>
+ <div id="delete">
+ <iframe src="deleteframe.html" id="delete-frame"></iframe>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/test_actions.html b/testing/web-platform/tests/webdriver/tests/support/html/test_actions.html
new file mode 100644
index 0000000000..e377840672
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/test_actions.html
@@ -0,0 +1,213 @@
+<!doctype html>
+<meta charset=utf-8>
+<html>
+<head>
+ <title>Test Actions</title>
+ <style>
+ div { padding: 0; margin: 0; }
+ #trackPointer { position: fixed; }
+ #resultContainer { width: 600px; height: 60px; }
+ .area { width: 100px; height: 50px; background-color: #ccc; }
+ .block { width: 5px; height: 5px; border: solid 1px red; }
+ .box { display: flex;}
+ #dragArea { position: relative; }
+ #dragTarget { position: absolute; top:22px; left:47px;}
+ </style>
+ <script>
+ "use strict";
+ var els = {};
+ var allEvents = { events: [] };
+ function displayMessage(message) {
+ document.getElementById("events").innerHTML = "<p>" + message + "</p>";
+ }
+
+ function appendMessage(message) {
+ document.getElementById("events").innerHTML += "<p>" + message + "</p>";
+ }
+
+ /**
+ * Escape |key| if it's in a surrogate-half character range.
+ *
+ * Example: given "\ud83d" return "U+d83d".
+ *
+ * Otherwise JSON.stringify will convert it to U+FFFD (REPLACEMENT CHARACTER)
+ * when returning a value from executeScript, for example.
+ */
+ function escapeSurrogateHalf(key) {
+ if (typeof key !== "undefined" && key.length === 1) {
+ var charCode = key.charCodeAt(0);
+ var highSurrogate = charCode >= 0xD800 && charCode <= 0xDBFF;
+ var surrogate = highSurrogate || (charCode >= 0xDC00 && charCode <= 0xDFFF);
+ if (surrogate) {
+ key = "U+" + charCode.toString(16);
+ }
+ }
+ return key;
+ }
+
+ function recordKeyboardEvent(event) {
+ var key = escapeSurrogateHalf(event.key);
+ allEvents.events.push({
+ "code": event.code,
+ "key": key,
+ "which": event.which,
+ "location": event.location,
+ "ctrl": event.ctrlKey,
+ "meta": event.metaKey,
+ "shift": event.shiftKey,
+ "repeat": event.repeat,
+ "type": event.type
+ });
+ appendMessage(event.type + " " +
+ "code: " + event.code + ", " +
+ "key: " + key + ", " +
+ "which: " + event.which + ", " +
+ "keyCode: " + event.keyCode);
+ }
+
+ function recordPointerEvent(event) {
+ if (event.type === "contextmenu") {
+ event.preventDefault();
+ }
+ allEvents.events.push({
+ "type": event.type,
+ "button": event.button,
+ "buttons": event.buttons,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "ctrlKey": event.ctrlKey,
+ "metaKey": event.metaKey,
+ "altKey": event.altKey,
+ "shiftKey": event.shiftKey,
+ "target": event.target.id
+ });
+ appendMessage(event.type + " " +
+ "button: " + event.button + ", " +
+ "pageX: " + event.pageX + ", " +
+ "pageY: " + event.pageY + ", " +
+ "button: " + event.button + ", " +
+ "buttons: " + event.buttons + ", " +
+ "ctrlKey: " + event.ctrlKey + ", " +
+ "altKey: " + event.altKey + ", " +
+ "metaKey: " + event.metaKey + ", " +
+ "shiftKey: " + event.shiftKey + ", " +
+ "target id: " + event.target.id);
+ }
+
+ function recordFirstPointerMove(event) {
+ recordPointerEvent(event);
+ window.removeEventListener("mousemove", recordFirstPointerMove);
+ }
+
+ function grabOnce(event) {
+ grab(event);
+ els.dragTarget.removeEventListener("mousedown", grabOnce);
+ }
+
+ function dropOnce(moveHandler) {
+ return function (event) {
+ moveHandler(event);
+ els.dragArea.removeEventListener("mouseup", dropOnce);
+ }
+ }
+
+ function resetEvents() {
+ allEvents.events.length = 0;
+ displayMessage("");
+ }
+
+ function drop(moveHandler) {
+ return function (event) {
+ els.dragArea.removeEventListener("mousemove", moveHandler);
+ els.dragTarget.style.backgroundColor = "yellow";
+ els.dragTarget.addEventListener("mousedown", grab);
+ recordPointerEvent(event);
+ };
+ }
+
+ function move(el, offsetX, offsetY, timeout) {
+ return function(event) {
+ setTimeout(function() {
+ el.style.top = event.clientY + offsetY + "px";
+ el.style.left = event.clientX + offsetX + "px";
+ }, timeout);
+ };
+ }
+
+ function grab(event) {
+ event.target.style.backgroundColor = "red";
+ let boxRect = event.target.getBoundingClientRect();
+ let areaRect = event.target.parentElement.getBoundingClientRect();
+ let moveHandler = move(
+ event.target,
+ // coordinates of dragTarget must be relative to dragArea such that
+ // dragTarget remains under the pointer
+ -(areaRect.left + (event.clientX - boxRect.left)),
+ -(areaRect.top + (event.clientY - boxRect.top)),
+ 20);
+ els.dragArea.addEventListener("mousemove", moveHandler);
+ els.dragArea.addEventListener("mouseup", dropOnce(drop(moveHandler)));
+ }
+
+ document.addEventListener("DOMContentLoaded", function() {
+ var keyReporter = document.getElementById("keys");
+ keyReporter.addEventListener("keyup", recordKeyboardEvent);
+ keyReporter.addEventListener("keypress", recordKeyboardEvent);
+ keyReporter.addEventListener("keydown", recordKeyboardEvent);
+
+ var outer = document.getElementById("outer");
+ outer.addEventListener("click", recordPointerEvent);
+ outer.addEventListener("dblclick", recordPointerEvent);
+ outer.addEventListener("mousedown", recordPointerEvent);
+ outer.addEventListener("mouseup", recordPointerEvent);
+ outer.addEventListener("contextmenu", recordPointerEvent);
+
+ window.addEventListener("mousemove", recordFirstPointerMove);
+ //visual cue for mousemove
+ var pointer = document.getElementById("trackPointer");
+ window.addEventListener("mousemove", move(pointer, 15, 15, 30));
+ // drag and drop
+ els.dragArea = document.getElementById("dragArea");
+ els.dragArea.addEventListener("dragstart", recordPointerEvent);
+ els.dragTarget = document.getElementById("dragTarget");
+ els.dragTarget.addEventListener("mousedown", grabOnce);
+
+ window.addEventListener("dragstart", recordPointerEvent);
+ window.addEventListener("dragenter", recordPointerEvent);
+ window.addEventListener("dragover", recordPointerEvent);
+ window.addEventListener("dragleave", recordPointerEvent);
+ window.addEventListener("drop", recordPointerEvent);
+ window.addEventListener("dragend", recordPointerEvent);
+ });
+ </script>
+</head>
+<body>
+ <div id="trackPointer" class="block"></div>
+ <div>
+ <h2>KeyReporter</h2>
+ <input type="text" id="keys" size="80">
+ </div>
+ <div>
+ <h2>ClickReporter</h2>
+ <div id="outer" class="area">
+ </div>
+ </div>
+ <div>
+ <h2>DragReporter</h2>
+ <div id="dragArea" class="area">
+ <div id="dragTarget" class="block"></div>
+ </div>
+ </div>
+ <div>
+ <h2>draggable</h2>
+ <div class=box>
+ <div id="draggable" draggable="true" class="area"></div>&nbsp;
+ <div id="droppable" ondrop="event.preventDefault()" ondragover="event.preventDefault()" class="area"></div>
+ </div>
+ </div>
+ <div id="resultContainer">
+ <h2>Events</h2>
+ <div id="events"></div>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/test_actions_pointer.html b/testing/web-platform/tests/webdriver/tests/support/html/test_actions_pointer.html
new file mode 100644
index 0000000000..dd169f0c5b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/test_actions_pointer.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<meta charset=utf-8>
+<html>
+<head>
+ <title>Test Actions</title>
+ <style>
+ div { padding: 0; margin: 0; }
+ #trackPointer { position: fixed; }
+ #resultContainer { width: 600px; height: 60px; }
+ .area { width: 100px; height: 50px; background-color: #ccc; }
+ </style>
+ <script>
+ "use strict";
+ var els = {};
+ var allEvents = { events: [] };
+ function displayMessage(message) {
+ document.getElementById("events").innerHTML = "<p>" + message + "</p>";
+ }
+
+ function appendMessage(message) {
+ document.getElementById("events").innerHTML += "<p>" + message + "</p>";
+ }
+
+ function recordPointerEvent(event) {
+ if (event.type === "contextmenu") {
+ event.preventDefault();
+ }
+ allEvents.events.push({
+ "type": event.type,
+ "button": event.button,
+ "buttons": event.buttons,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "ctrlKey": event.ctrlKey,
+ "metaKey": event.metaKey,
+ "altKey": event.altKey,
+ "shiftKey": event.shiftKey,
+ "target": event.target.id,
+ "pointerType": event.pointerType,
+ "width": event.width,
+ "height": event.height,
+ "pressure": event.pressure,
+ "tangentialPressure": event.tangentialPressure,
+ "tiltX": event.tiltX,
+ "tiltY": event.tiltY,
+ "twist": event.twist,
+ "altitudeAngle": event.altitudeAngle,
+ "azimuthAngle": event.azimuthAngle
+ });
+ appendMessage(event.type + " " +
+ "button: " + event.button + ", " +
+ "pageX: " + event.pageX + ", " +
+ "pageY: " + event.pageY + ", " +
+ "button: " + event.button + ", " +
+ "buttons: " + event.buttons + ", " +
+ "ctrlKey: " + event.ctrlKey + ", " +
+ "altKey: " + event.altKey + ", " +
+ "metaKey: " + event.metaKey + ", " +
+ "shiftKey: " + event.shiftKey + ", " +
+ "target id: " + event.target.id + ", " +
+ "pointerType: " + event.pointerType + ", " +
+ "width: " + event.width + ", " +
+ "height: " + event.height + ", " +
+ "pressure: " + event.pressure + ", " +
+ "tangentialPressure: " + event.tangentialPressure + ", " +
+ "tiltX: " + event.tiltX + ", " +
+ "tiltY: " + event.tiltY + ", " +
+ "twist: " + event.twist + ", " +
+ "altitudeAngle: " + event.altitudeAngle + ", " +
+ "azimuthAngle: " + event.azimuthAngle);
+ }
+
+ function resetEvents() {
+ allEvents.events.length = 0;
+ displayMessage("");
+ }
+
+ document.addEventListener("DOMContentLoaded", function() {
+ var pointerArea = document.getElementById("pointerArea");
+ pointerArea.addEventListener("pointerdown", recordPointerEvent);
+ pointerArea.addEventListener("pointermove", recordPointerEvent);
+ pointerArea.addEventListener("pointerup", recordPointerEvent);
+ pointerArea.addEventListener("pointerover", recordPointerEvent);
+ pointerArea.addEventListener("pointerenter", recordPointerEvent);
+ pointerArea.addEventListener("pointerout", recordPointerEvent);
+ pointerArea.addEventListener("pointerleave", recordPointerEvent);
+ });
+ </script>
+</head>
+<body>
+ <div id="trackPointer" class="block"></div>
+ <div>
+ <h2>PointerReporter</h2>
+ <div id="pointerArea" class="area">
+ </div>
+ </div>
+ <div id="resultContainer">
+ <h2>Events</h2>
+ <div id="events"></div>
+ </div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html b/testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html
new file mode 100644
index 0000000000..db5952ed74
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/html/test_actions_scroll.html
@@ -0,0 +1,139 @@
+<!doctype html>
+<meta charset=utf-8>
+<html>
+ <head>
+ <title>Test Actions</title>
+ <style>
+ div {
+ padding: 0;
+ margin: 0;
+ }
+
+ #not-scrollable {
+ margin-bottom: 100px;
+ width: 100px;
+ height: 50px;
+ }
+
+ #not-scrollable-content {
+ width: 200px;
+ height: 100px;
+ background-color: #ccc;
+ }
+
+ #scrollable {
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+ }
+
+ #scrollable-content {
+ width: 600px;
+ height: 1000px;
+ background-color: blue;
+ }
+
+ #iframe {
+ width: 100px;
+ height: 100px;
+ }
+
+ #event-reporter {
+ white-space: pre-line;
+ }
+ </style>
+
+ <script>
+ var eventReporter;
+ var allEvents = { events: [] };
+
+ function addMessage(message) {
+ eventReporter.textContent = `${message}\n${eventReporter.textContent}`;
+ }
+
+ function recordWheelEvent(event) {
+ allEvents.events.push({
+ "type": event.type,
+ "button": event.button,
+ "buttons": event.buttons,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "deltaX": event.deltaX,
+ "deltaY": event.deltaY,
+ "deltaZ": event.deltaZ,
+ "deltaMode": event.deltaMode,
+ "target": event.target.id,
+ });
+
+ addMessage(
+ "type: " + event.type + " " +
+ "button: " + event.button + ", " +
+ "buttons: " + event.buttons + ", " +
+ "pageX: " + event.pageX + ", " +
+ "pageY: " + event.pageY + ", " +
+ "deltaX: " + event.deltaX + ", " +
+ "deltaY: " + event.deltaY + ", " +
+ "deltaZ: " + event.deltaZ + ", " +
+ "deltaMode: " + event.deltaMode + ", " +
+ "target id: " + event.target.id
+ );
+ }
+
+ document.addEventListener("DOMContentLoaded", function () {
+ eventReporter = document.getElementById("event-reporter");
+
+ var noScroll = document.getElementById("not-scrollable");
+ noScroll.addEventListener("wheel", recordWheelEvent);
+
+ var scrollable = document.getElementById("scrollable");
+ scrollable.addEventListener("wheel", recordWheelEvent);
+ });
+ </script>
+ </head>
+
+ <body>
+ <div>
+ <h2>Scroll Reporter</h2>
+ <div id="not-scrollable">
+ <div id="not-scrollable-content"></div>
+ </div>
+ </div>
+
+ <div>
+ <h2>Overflow Scroll Reporter</h2>
+ <div id="scrollable">
+ <div id="scrollable-content"></div>
+ </div>
+ </div>
+
+ <div>
+ <h2>iframe Scroll Reporter</h2>
+ <iframe id="iframe" srcdoc='
+ <script>
+ document.scrollingElement.addEventListener("wheel", event => {
+ window.parent.recordWheelEvent({
+ "type": event.type,
+ "button": event.button,
+ "buttons": event.buttons,
+ "pageX": event.pageX,
+ "pageY": event.pageY,
+ "deltaX": event.deltaX,
+ "deltaY": event.deltaY,
+ "deltaZ": event.deltaZ,
+ "deltaMode": event.deltaMode,
+ "target": event.target
+ });
+ });
+ </script>
+ <div id="iframeContent" style="width: 7500px; height: 7500px; background-color:blue">
+ </div>'>
+ </iframe>
+ </div>
+
+ <div id="resultContainer">
+ <hr />
+ <h2>Events</h2>
+ <div id="event-reporter"></div>
+ </div>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_handlers/__init__.py b/testing/web-platform/tests/webdriver/tests/support/http_handlers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_handlers/__init__.py
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_handlers/authentication.py b/testing/web-platform/tests/webdriver/tests/support/http_handlers/authentication.py
new file mode 100644
index 0000000000..acee2d18d6
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_handlers/authentication.py
@@ -0,0 +1,37 @@
+from urllib.parse import urlencode
+
+
+def basic_authentication(url, **kwargs):
+ query = {}
+
+ return url("/webdriver/tests/support/http_handlers/authentication.py",
+ query=urlencode(query),
+ **kwargs)
+
+
+def main(request, response):
+ username = request.auth.username
+ password = request.auth.password
+
+ expected_username = "user"
+ if b"username" in request.GET:
+ expected_username = request.GET.first(b"username")
+
+ expected_password = "password"
+ if b"password" in request.GET:
+ expected_password = request.GET.first(b"password")
+
+ if username == expected_username and password == expected_password:
+ if b"contenttype" in request.GET:
+ content_type = request.GET.first(b"contenttype")
+ response.headers.set(b"Content-Type", content_type)
+
+ return b"Authentication done"
+
+ realm = b"test"
+ if b"realm" in request.GET:
+ realm = request.GET.first(b"realm")
+
+ return ((401, b"Unauthorized"),
+ [(b"WWW-Authenticate", b'Basic realm="' + realm + b'"')],
+ f"Please login with credentials '{expected_username}' and '{expected_password}'")
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_handlers/cached.py b/testing/web-platform/tests/webdriver/tests/support/http_handlers/cached.py
new file mode 100644
index 0000000000..a43410f885
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_handlers/cached.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ """Simple handler that returns a response with Cache-Control max-age=3600.
+ """
+
+ status = int(request.GET.get(b"status", None))
+ # For redirects, a "location" get parameter can indicate the redirected url
+ if status == 301 and b"location" in request.GET:
+ response.headers.set(b"Location", request.GET.first(b"location"))
+
+ response.status = status
+ response.headers.set(b"Content-Type", "text/plain")
+ response.headers.set(b"Expires", "Thu, 01 Dec 2100 20:00:00 GMT")
+ response.headers.set(b"Cache-Control", "max-age=3600")
+ return "Cached HTTP Response"
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_handlers/headers.py b/testing/web-platform/tests/webdriver/tests/support/http_handlers/headers.py
new file mode 100644
index 0000000000..cb8d18d964
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_handlers/headers.py
@@ -0,0 +1,22 @@
+def main(request, response):
+ """Simple handler that returns a response with custom headers.
+
+ The request should define at least one "header" query parameter, with the
+ format {key}:{value}. For instance ?header=foo:bar will create a response
+ with a header with the key "foo" and the value "bar". Additional headers
+ can be set by passing more "header" query parameters.
+ """
+ response.status = 200
+ if b"header" in request.GET:
+ try:
+ headers = request.GET.get_list(b"header")
+ for header in headers:
+ header_parts = header.split(b":")
+ response.headers.set(header_parts[0], header_parts[1])
+ except ValueError:
+ pass
+
+ if b"Content-Type" not in response.headers:
+ response.headers.set(b"Content-Type", "text/plain")
+
+ response.content = "HTTP Response Headers"
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_handlers/must-revalidate.py b/testing/web-platform/tests/webdriver/tests/support/http_handlers/must-revalidate.py
new file mode 100644
index 0000000000..94f5a795a2
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_handlers/must-revalidate.py
@@ -0,0 +1,17 @@
+def main(request, response):
+ """Simple handler that returns a response with Cache-Control max-age=0 and
+ must-revalidate.
+ The request can include a return-304 header to trigger the handler to return
+ a 304 instead of a 200.
+ """
+ response.headers.set(b"Content-Type", "text/plain")
+
+ if b"true" == request.headers.get(b"return-304", None):
+ # instruct the browser that the response was not modified and the cache
+ # can be used.
+ response.status = 304
+ return ""
+ else:
+ response.headers.set(b"Cache-Control", b"max-age=0, must-revalidate")
+ response.status = 200
+ return "must-revalidate HTTP Response"
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_handlers/redirect.py b/testing/web-platform/tests/webdriver/tests/support/http_handlers/redirect.py
new file mode 100644
index 0000000000..f2fd1ebd51
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_handlers/redirect.py
@@ -0,0 +1,19 @@
+def main(request, response):
+ """Simple handler that causes redirection.
+
+ The request should typically have two query parameters:
+ status - The status to use for the redirection. Defaults to 302.
+ location - The resource to redirect to.
+ """
+ status = 302
+ if b"status" in request.GET:
+ try:
+ status = int(request.GET.first(b"status"))
+ except ValueError:
+ pass
+
+ response.status = status
+
+ location = request.GET.first(b"location")
+
+ response.headers.set(b"Location", location)
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_handlers/status.py b/testing/web-platform/tests/webdriver/tests/support/http_handlers/status.py
new file mode 100644
index 0000000000..4dc3de0a88
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_handlers/status.py
@@ -0,0 +1,16 @@
+def main(request, response):
+ """Simple handler that returns a response with a custom status.
+
+ The request expects a "status" query parameter, which should be a number.
+ If no status is provided, status 200 will be used.
+ """
+ status = 200
+ if b"status" in request.GET:
+ try:
+ status = int(request.GET.first(b"status"))
+ except ValueError:
+ pass
+
+ response.status = status
+ response.headers.set(b"Content-Type", "text/plain")
+ response.content = "HTTP Response Status"
diff --git a/testing/web-platform/tests/webdriver/tests/support/http_request.py b/testing/web-platform/tests/webdriver/tests/support/http_request.py
new file mode 100644
index 0000000000..a936d7f1f0
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/http_request.py
@@ -0,0 +1,40 @@
+import contextlib
+import json
+
+from http.client import HTTPConnection
+
+
+class HTTPRequest(object):
+ def __init__(self, host: str, port: int):
+ self.host = host
+ self.port = port
+
+ def head(self, path: str):
+ return self._request("HEAD", path)
+
+ def get(self, path: str):
+ return self._request("GET", path)
+
+ def post(self, path: str, body):
+ return self._request("POST", path, body)
+
+ @contextlib.contextmanager
+ def _request(self, method: str, path: str, body=None):
+ payload = None
+
+ if body is not None:
+ try:
+ payload = json.dumps(body)
+ except ValueError:
+ raise ValueError("Failed to encode request body as JSON: {}".format(
+ json.dumps(body, indent=2)))
+
+ if isinstance(payload, str):
+ payload = body.encode("utf-8")
+
+ conn = HTTPConnection(self.host, self.port)
+ try:
+ conn.request(method, path, payload)
+ yield conn.getresponse()
+ finally:
+ conn.close()
diff --git a/testing/web-platform/tests/webdriver/tests/support/image.py b/testing/web-platform/tests/webdriver/tests/support/image.py
new file mode 100644
index 0000000000..533b8b2068
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/image.py
@@ -0,0 +1,40 @@
+import struct
+from typing import NamedTuple, Tuple
+
+from tests.support.asserts import assert_png
+
+
+inch_in_cm = 2.54
+inch_in_pixel = 96
+inch_in_point = 72
+
+
+def cm_to_px(cm: float) -> float:
+ return round(cm * inch_in_pixel / inch_in_cm)
+
+
+def px_to_cm(px: float) -> float:
+ return px * inch_in_cm / inch_in_pixel
+
+
+def pt_to_cm(pt: float) -> float:
+ return pt * inch_in_cm / inch_in_point
+
+
+def png_dimensions(screenshot) -> Tuple[int, int]:
+ image = assert_png(screenshot)
+ width, height = struct.unpack(">LL", image[16:24])
+ return int(width), int(height)
+
+
+class ImageDifference(NamedTuple):
+ """Summary of the pixel-level differences between two images."""
+
+ """The total number of pixel differences between the images"""
+ total_pixels: int
+
+ """The maximum difference between any corresponding color channels across all pixels of the image"""
+ max_difference: int
+
+ def equal(self) -> bool:
+ return self.total_pixels == 0
diff --git a/testing/web-platform/tests/webdriver/tests/support/inline.py b/testing/web-platform/tests/webdriver/tests/support/inline.py
new file mode 100644
index 0000000000..ecb2a2587b
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/inline.py
@@ -0,0 +1,72 @@
+"""Helpers for inlining extracts of documents in tests."""
+
+from typing import Optional
+from urllib.parse import urlencode
+
+
+BOILERPLATES = {
+ "html": "<!doctype html>\n<meta charset={charset}>\n{src}",
+ "xhtml": """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <title>XHTML might be the future</title>
+ </head>
+
+ <body>
+ {src}
+ </body>
+</html>""",
+ "xml": """<?xml version="1.0" encoding="{charset}"?>\n{src}""",
+ "js": "",
+}
+MIME_TYPES = {
+ "html": "text/html",
+ "xhtml": "application/xhtml+xml",
+ "xml": "text/xml",
+ "js": "text/javascript",
+}
+
+
+def build_inline(build_url, src,
+ doctype: str = "html",
+ mime: Optional[str] = None, charset: Optional[str] = None,
+ parameters=None, **kwargs):
+ if mime is None:
+ mime = MIME_TYPES[doctype]
+ if charset is None:
+ charset = "UTF-8"
+ if parameters is None:
+ parameters = {}
+
+ doc = BOILERPLATES[doctype].format(charset=charset, src=src)
+
+ query = {"doc": doc, "mime": mime, "charset": charset}
+ query.update(parameters)
+
+ return build_url(
+ "/webdriver/tests/support/inline.py",
+ query=urlencode(query),
+ **kwargs)
+
+
+def main(request, response):
+ doc = request.GET.first(b"doc", None)
+ mime = request.GET.first(b"mime", None)
+ charset = request.GET.first(b"charset", None)
+
+ if doc is None:
+ return 404, [(b"Content-Type",
+ b"text/plain")], b"Missing doc parameter in query"
+
+ content_type = []
+ if mime is not None:
+ content_type.append(mime)
+ if charset is not None:
+ content_type.append(b"charset=%s" % charset)
+
+ headers = {b"X-XSS-Protection": b"0"}
+ if len(content_type) > 0:
+ headers[b"Content-Type"] = b";".join(content_type)
+
+ return 200, headers.items(), doc
diff --git a/testing/web-platform/tests/webdriver/tests/support/keys.py b/testing/web-platform/tests/webdriver/tests/support/keys.py
new file mode 100644
index 0000000000..b7b7598b65
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/keys.py
@@ -0,0 +1,904 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""The Keys implementation."""
+
+from collections import OrderedDict
+from inspect import getmembers
+
+
+class Keys(object):
+ """
+ Set of special keys codes.
+
+ See also https://w3c.github.io/webdriver/#keyboard-actions
+ """
+
+ NULL = u"\ue000"
+ CANCEL = u"\ue001" # ^break
+ HELP = u"\ue002"
+ BACKSPACE = u"\ue003"
+ TAB = u"\ue004"
+ CLEAR = u"\ue005"
+ RETURN = u"\ue006"
+ ENTER = u"\ue007"
+ SHIFT = u"\ue008"
+ CONTROL = u"\ue009"
+ ALT = u"\ue00a"
+ PAUSE = u"\ue00b"
+ ESCAPE = u"\ue00c"
+ SPACE = u"\ue00d"
+ PAGE_UP = u"\ue00e"
+ PAGE_DOWN = u"\ue00f"
+ END = u"\ue010"
+ HOME = u"\ue011"
+ LEFT = u"\ue012"
+ UP = u"\ue013"
+ RIGHT = u"\ue014"
+ DOWN = u"\ue015"
+ INSERT = u"\ue016"
+ DELETE = u"\ue017"
+ SEMICOLON = u"\ue018"
+ EQUALS = u"\ue019"
+
+ NUMPAD0 = u"\ue01a" # number pad keys
+ NUMPAD1 = u"\ue01b"
+ NUMPAD2 = u"\ue01c"
+ NUMPAD3 = u"\ue01d"
+ NUMPAD4 = u"\ue01e"
+ NUMPAD5 = u"\ue01f"
+ NUMPAD6 = u"\ue020"
+ NUMPAD7 = u"\ue021"
+ NUMPAD8 = u"\ue022"
+ NUMPAD9 = u"\ue023"
+ MULTIPLY = u"\ue024"
+ ADD = u"\ue025"
+ SEPARATOR = u"\ue026"
+ SUBTRACT = u"\ue027"
+ DECIMAL = u"\ue028"
+ DIVIDE = u"\ue029"
+
+ F1 = u"\ue031" # function keys
+ F2 = u"\ue032"
+ F3 = u"\ue033"
+ F4 = u"\ue034"
+ F5 = u"\ue035"
+ F6 = u"\ue036"
+ F7 = u"\ue037"
+ F8 = u"\ue038"
+ F9 = u"\ue039"
+ F10 = u"\ue03a"
+ F11 = u"\ue03b"
+ F12 = u"\ue03c"
+
+ META = u"\ue03d"
+
+ # More keys from webdriver spec
+ ZENKAKUHANKAKU = u"\uE040"
+ R_SHIFT = u"\uE050"
+ R_CONTROL = u"\uE051"
+ R_ALT = u"\uE052"
+ R_META = u"\uE053"
+ R_PAGEUP = u"\uE054"
+ R_PAGEDOWN = u"\uE055"
+ R_END = u"\uE056"
+ R_HOME = u"\uE057"
+ R_ARROWLEFT = u"\uE058"
+ R_ARROWUP = u"\uE059"
+ R_ARROWRIGHT = u"\uE05A"
+ R_ARROWDOWN = u"\uE05B"
+ R_INSERT = u"\uE05C"
+ R_DELETE = u"\uE05D"
+
+
+ALL_KEYS = getmembers(Keys, lambda x: type(x) is str)
+
+ALL_EVENTS = OrderedDict(
+ [
+ ("ADD", OrderedDict(
+ [
+ ("code", "NumpadAdd"),
+ ("ctrl", False),
+ ("key", "+"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue025")
+ ]
+ )),
+ ("ALT", OrderedDict(
+ [
+ ("code", "AltLeft"),
+ ("ctrl", False),
+ ("key", "Alt"),
+ ("location", 1),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue00a")
+ ]
+ )),
+ ("BACKSPACE", OrderedDict(
+ [
+ ("code", "Backspace"),
+ ("ctrl", False),
+ ("key", "Backspace"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue003")
+ ]
+ )),
+ ("CANCEL", OrderedDict(
+ [
+ ("code", ""),
+ ("ctrl", False),
+ ("key", "Cancel"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue001")
+ ]
+ )),
+ ("CLEAR", OrderedDict(
+ [
+ ("code", ""),
+ ("ctrl", False),
+ ("key", "Clear"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue005")
+ ]
+ )),
+ ("CONTROL", OrderedDict(
+ [
+ ("code", "ControlLeft"),
+ ("ctrl", True),
+ ("key", "Control"),
+ ("location", 1),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue009")
+ ]
+ )),
+ ("DECIMAL", OrderedDict(
+ [
+ ("code", "NumpadDecimal"),
+ ("ctrl", False),
+ ("key", "."),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue028")
+ ]
+ )),
+ ("DELETE", OrderedDict(
+ [
+ ("code", "Delete"),
+ ("ctrl", False),
+ ("key", "Delete"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue017")
+ ]
+ )),
+ ("DIVIDE", OrderedDict(
+ [
+ ("code", "NumpadDivide"),
+ ("ctrl", False),
+ ("key", "/"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue029")
+ ]
+ )),
+ ("DOWN", OrderedDict(
+ [
+ ("code", "ArrowDown"),
+ ("ctrl", False),
+ ("key", "ArrowDown"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue015")
+ ]
+ )),
+ ("END", OrderedDict(
+ [
+ ("code", "End"),
+ ("ctrl", False),
+ ("key", "End"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue010")
+ ]
+ )),
+ ("ENTER", OrderedDict(
+ [
+ ("code", "NumpadEnter"),
+ ("ctrl", False),
+ ("key", "Enter"),
+ ("location", 1),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue007")
+ ]
+ )),
+ ("EQUALS", OrderedDict(
+ [
+ ("code", "NumpadEqual"),
+ ("ctrl", False),
+ ("key", "="),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue019")
+ ]
+ )),
+ ("ESCAPE", OrderedDict(
+ [
+ ("code", "Escape"),
+ ("ctrl", False),
+ ("key", "Escape"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue00c")
+ ]
+ )),
+ ("F1", OrderedDict(
+ [
+ ("code", "F1"),
+ ("ctrl", False),
+ ("key", "F1"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue031")
+ ]
+ )),
+ ("F10", OrderedDict(
+ [
+ ("code", "F10"),
+ ("ctrl", False),
+ ("key", "F10"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue03a")
+ ]
+ )),
+ ("F11", OrderedDict(
+ [
+ ("code", "F11"),
+ ("ctrl", False),
+ ("key", "F11"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue03b")
+ ]
+ )),
+ ("F12", OrderedDict(
+ [
+ ("code", "F12"),
+ ("ctrl", False),
+ ("key", "F12"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue03c")
+ ]
+ )),
+ ("F2", OrderedDict(
+ [
+ ("code", "F2"),
+ ("ctrl", False),
+ ("key", "F2"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue032")
+ ]
+ )),
+ ("F3", OrderedDict(
+ [
+ ("code", "F3"),
+ ("ctrl", False),
+ ("key", "F3"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue033")
+ ]
+ )),
+ ("F4", OrderedDict(
+ [
+ ("code", "F4"),
+ ("ctrl", False),
+ ("key", "F4"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue034")
+ ]
+ )),
+ ("F5", OrderedDict(
+ [
+ ("code", "F5"),
+ ("ctrl", False),
+ ("key", "F5"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue035")
+ ]
+ )),
+ ("F6", OrderedDict(
+ [
+ ("code", "F6"),
+ ("ctrl", False),
+ ("key", "F6"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue036")
+ ]
+ )),
+ ("F7", OrderedDict(
+ [
+ ("code", "F7"),
+ ("ctrl", False),
+ ("key", "F7"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue037")
+ ]
+ )),
+ ("F8", OrderedDict(
+ [
+ ("code", "F8"),
+ ("ctrl", False),
+ ("key", "F8"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue038")
+ ]
+ )),
+ ("F9", OrderedDict(
+ [
+ ("code", "F9"),
+ ("ctrl", False),
+ ("key", "F9"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue039")
+ ]
+ )),
+ ("HELP", OrderedDict(
+ [
+ ("code", "Help"),
+ ("ctrl", False),
+ ("key", "Help"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue002")
+ ]
+ )),
+ ("HOME", OrderedDict(
+ [
+ ("code", "Home"),
+ ("ctrl", False),
+ ("key", "Home"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue011")
+ ]
+ )),
+ ("INSERT", OrderedDict(
+ [
+ ("code", "Insert"),
+ ("ctrl", False),
+ ("key", "Insert"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue016")
+ ]
+ )),
+ ("LEFT", OrderedDict(
+ [
+ ("code", "ArrowLeft"),
+ ("ctrl", False),
+ ("key", "ArrowLeft"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue012")
+ ]
+ )),
+ ("META", OrderedDict(
+ [
+ ("code", "MetaLeft"),
+ ("ctrl", False),
+ ("key", "Meta"),
+ ("location", 1),
+ ("meta", True),
+ ("shift", False),
+ ("value", u"\ue03d")
+ ]
+ )),
+ ("MULTIPLY", OrderedDict(
+ [
+ ("code", "NumpadMultiply"),
+ ("ctrl", False),
+ ("key", "*"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue024")
+ ]
+ )),
+ ("NULL", OrderedDict(
+ [
+ ("code", ""),
+ ("ctrl", False),
+ ("key", "Unidentified"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue000")
+ ]
+ )),
+ ("NUMPAD0", OrderedDict(
+ [
+ ("code", "Numpad0"),
+ ("ctrl", False),
+ ("key", "0"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue01a")
+ ]
+ )),
+ ("NUMPAD1", OrderedDict(
+ [
+ ("code", "Numpad1"),
+ ("ctrl", False),
+ ("key", "1"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue01b")
+ ]
+ )),
+ ("NUMPAD2", OrderedDict(
+ [
+ ("code", "Numpad2"),
+ ("ctrl", False),
+ ("key", "2"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue01c")
+ ]
+ )),
+ ("NUMPAD3", OrderedDict(
+ [
+ ("code", "Numpad3"),
+ ("ctrl", False),
+ ("key", "3"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue01d")
+ ]
+ )),
+ ("NUMPAD4", OrderedDict(
+ [
+ ("code", "Numpad4"),
+ ("ctrl", False),
+ ("key", "4"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue01e")
+ ]
+ )),
+ ("NUMPAD5", OrderedDict(
+ [
+ ("code", "Numpad5"),
+ ("ctrl", False),
+ ("key", "5"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue01f")
+ ]
+ )),
+ ("NUMPAD6", OrderedDict(
+ [
+ ("code", "Numpad6"),
+ ("ctrl", False),
+ ("key", "6"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue020")
+ ]
+ )),
+ ("NUMPAD7", OrderedDict(
+ [
+ ("code", "Numpad7"),
+ ("ctrl", False),
+ ("key", "7"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue021")
+ ]
+ )),
+ ("NUMPAD8", OrderedDict(
+ [
+ ("code", "Numpad8"),
+ ("ctrl", False),
+ ("key", "8"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue022")
+ ]
+ )),
+ ("NUMPAD9", OrderedDict(
+ [
+ ("code", "Numpad9"),
+ ("ctrl", False),
+ ("key", "9"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue023")
+ ]
+ )),
+ ("PAGE_DOWN", OrderedDict(
+ [
+ ("code", "PageDown"),
+ ("ctrl", False),
+ ("key", "PageDown"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue00f")
+ ]
+ )),
+ ("PAGE_UP", OrderedDict(
+ [
+ ("code", "PageUp"),
+ ("ctrl", False),
+ ("key", "PageUp"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue00e")
+ ]
+ )),
+ ("PAUSE", OrderedDict(
+ [
+ ("code", "Pause"),
+ ("ctrl", False),
+ ("key", "Pause"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue00b")
+ ]
+ )),
+ ("RETURN", OrderedDict(
+ [
+ ("code", "Enter"),
+ ("ctrl", False),
+ ("key", "Enter"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue006")
+ ]
+ )),
+ ("RIGHT", OrderedDict(
+ [
+ ("code", "ArrowRight"),
+ ("ctrl", False),
+ ("key", "ArrowRight"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue014")
+ ]
+ )),
+ ("R_ALT", OrderedDict(
+ [
+ ("code", "AltRight"),
+ ("ctrl", False),
+ ("key", "Alt"),
+ ("location", 2),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue052")
+ ]
+ )),
+ ("R_ARROWDOWN", OrderedDict(
+ [
+ ("code", "Numpad2"),
+ ("ctrl", False),
+ ("key", "ArrowDown"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue05b")
+ ]
+ )),
+ ("R_ARROWLEFT", OrderedDict(
+ [
+ ("code", "Numpad4"),
+ ("ctrl", False),
+ ("key", "ArrowLeft"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue058")
+ ]
+ )),
+ ("R_ARROWRIGHT", OrderedDict(
+ [
+ ("code", "Numpad6"),
+ ("ctrl", False),
+ ("key", "ArrowRight"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue05a")
+ ]
+ )),
+ ("R_ARROWUP", OrderedDict(
+ [
+ ("code", "Numpad8"),
+ ("ctrl", False),
+ ("key", "ArrowUp"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue059")
+ ]
+ )),
+ ("R_CONTROL", OrderedDict(
+ [
+ ("code", "ControlRight"),
+ ("ctrl", True),
+ ("key", "Control"),
+ ("location", 2),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue051")
+ ]
+ )),
+ ("R_DELETE", OrderedDict(
+ [
+ ("code", "NumpadDecimal"),
+ ("ctrl", False),
+ ("key", "Delete"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue05d")
+ ]
+ )),
+ ("R_END", OrderedDict(
+ [
+ ("code", "Numpad1"),
+ ("ctrl", False),
+ ("key", "End"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue056")
+ ]
+ )),
+ ("R_HOME", OrderedDict(
+ [
+ ("code", "Numpad7"),
+ ("ctrl", False),
+ ("key", "Home"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue057")
+ ]
+ )),
+ ("R_INSERT", OrderedDict(
+ [
+ ("code", "Numpad0"),
+ ("ctrl", False),
+ ("key", "Insert"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue05c")
+ ]
+ )),
+ ("R_META", OrderedDict(
+ [
+ ("code", "MetaRight"),
+ ("ctrl", False),
+ ("key", "Meta"),
+ ("location", 2),
+ ("meta", True),
+ ("shift", False),
+ ("value", u"\ue053")
+ ]
+ )),
+ ("R_PAGEDOWN", OrderedDict(
+ [
+ ("code", "Numpad3"),
+ ("ctrl", False),
+ ("key", "PageDown"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue055")
+ ]
+ )),
+ ("R_PAGEUP", OrderedDict(
+ [
+ ("code", "Numpad9"),
+ ("ctrl", False),
+ ("key", "PageUp"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue054")
+ ]
+ )),
+ ("R_SHIFT", OrderedDict(
+ [
+ ("code", "ShiftRight"),
+ ("ctrl", False),
+ ("key", "Shift"),
+ ("location", 2),
+ ("meta", False),
+ ("shift", True),
+ ("value", u"\ue050")
+ ]
+ )),
+ ("SEMICOLON", OrderedDict(
+ [
+ ("code", ""),
+ ("ctrl", False),
+ ("key", ";"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue018")
+ ]
+ )),
+ ("SEPARATOR", OrderedDict(
+ [
+ ("code", "NumpadComma"),
+ ("ctrl", False),
+ ("key", ","),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue026")
+ ]
+ )),
+ ("SHIFT", OrderedDict(
+ [
+ ("code", "ShiftLeft"),
+ ("ctrl", False),
+ ("key", "Shift"),
+ ("location", 1),
+ ("meta", False),
+ ("shift", True),
+ ("value", u"\ue008")
+ ]
+ )),
+ ("SPACE", OrderedDict(
+ [
+ ("code", "Space"),
+ ("ctrl", False),
+ ("key", " "),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue00d")
+ ]
+ )),
+ ("SUBTRACT", OrderedDict(
+ [
+ ("code", "NumpadSubtract"),
+ ("ctrl", False),
+ ("key", "-"),
+ ("location", 3),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue027")
+ ]
+ )),
+ ("TAB", OrderedDict(
+ [
+ ("code", "Tab"),
+ ("ctrl", False),
+ ("key", "Tab"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue004")
+ ]
+ )),
+ ("UP", OrderedDict(
+ [
+ ("code", "ArrowUp"),
+ ("ctrl", False),
+ ("key", "ArrowUp"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue013")
+ ]
+ )),
+ ("ZENKAKUHANKAKU", OrderedDict(
+ [
+ ("code", ""),
+ ("ctrl", False),
+ ("key", "ZenkakuHankaku"),
+ ("location", 0),
+ ("meta", False),
+ ("shift", False),
+ ("value", u"\ue040")
+ ]
+ ))
+ ]
+)
+
+ALTERNATIVE_KEY_NAMES = {
+ "ADD": "Add",
+ "DECIMAL": "Decimal",
+ "DELETE": "Del",
+ "DIVIDE": "Divide",
+ "DOWN": "Down",
+ "ESCAPE": "Esc",
+ "LEFT": "Left",
+ "MULTIPLY": "Multiply",
+ "R_ARROWDOWN": "Down",
+ "R_ARROWLEFT": "Left",
+ "R_ARROWRIGHT": "Right",
+ "R_ARROWUP": "Up",
+ "R_DELETE": "Del",
+ "RIGHT": "Right",
+ "SEPARATOR": "Separator",
+ "SPACE": "Spacebar",
+ "SUBTRACT": "Subtract",
+ "UP": "Up",
+}
diff --git a/testing/web-platform/tests/webdriver/tests/support/screenshot.py b/testing/web-platform/tests/webdriver/tests/support/screenshot.py
new file mode 100644
index 0000000000..374e5ed539
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/screenshot.py
@@ -0,0 +1,50 @@
+DEFAULT_CONTENT = "<div id='content'>Lorem ipsum dolor sit amet.</div>"
+
+REFERENCE_CONTENT = f"<div id='outer'>{DEFAULT_CONTENT}</div>"
+REFERENCE_STYLE = """
+ <style>
+ #outer {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 200px;
+ height: 200px;
+ }
+ #content {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ background: green;
+ }
+ </style>
+"""
+
+OUTER_IFRAME_STYLE = """
+ <style>
+ iframe {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 200px;
+ height: 200px;
+ }
+ </style>
+"""
+
+INNER_IFRAME_STYLE = """
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ display: block;
+ margin: 0;
+ border: 0;
+ width: 100px;
+ height: 100px;
+ background: green;
+ }
+ </style>
+"""
diff --git a/testing/web-platform/tests/webdriver/tests/support/sync.py b/testing/web-platform/tests/webdriver/tests/support/sync.py
new file mode 100644
index 0000000000..4e92bcdf23
--- /dev/null
+++ b/testing/web-platform/tests/webdriver/tests/support/sync.py
@@ -0,0 +1,279 @@
+import asyncio
+import collections
+import inspect
+import sys
+import time
+
+from webdriver import error
+
+
+DEFAULT_TIMEOUT = 5
+DEFAULT_INTERVAL = 0.1
+
+
+class Poll(object):
+ """
+ An explicit conditional utility primitive for polling until a
+ condition evaluates to something truthy.
+
+ A `Poll` instance defines the maximum amount of time to wait
+ for a condition, as well as the frequency with which to check
+ the condition. Furthermore, the user may configure the wait
+ to ignore specific types of exceptions whilst waiting, such as
+ `error.NoSuchElementException` when searching for an element
+ on the page.
+ """
+
+ def __init__(self,
+ session,
+ timeout=DEFAULT_TIMEOUT,
+ interval=DEFAULT_INTERVAL,
+ raises=error.TimeoutException,
+ message=None,
+ ignored_exceptions=None,
+ clock=time):
+ """
+ Configure the poller to have a custom timeout, interval,
+ and list of ignored exceptions. Optionally a different time
+ implementation than the one provided by the standard library
+ (`time`) can also be provided.
+
+ Sample usage::
+
+ # Wait 30 seconds for window to open,
+ # checking for its presence once every 5 seconds.
+ from support.sync import Poll
+ wait = Poll(session, timeout=30, interval=5,
+ ignored_exceptions=error.NoSuchWindowException)
+ window = wait.until(lambda s: s.switch_to_window(42))
+
+ :param session: The input value to be provided to conditions,
+ usually a `webdriver.Session` instance.
+
+ :param timeout: How long to wait for the evaluated condition
+ to become true.
+
+ :param interval: How often the condition should be evaluated.
+ In reality the interval may be greater as the cost of
+ evaluating the condition function. If that is not the case the
+ interval for the next condition function call is shortend to keep
+ the original interval sequence as best as possible.
+
+ :param raises: Optional exception to raise when poll elapses.
+ If not used, an `error.TimeoutException` is raised.
+ If it is `None`, no exception is raised on the poll elapsing.
+
+ :param message: An optional message to include in `raises`'s
+ message if the `until` condition times out.
+
+ :param ignored_exceptions: Ignore specific types of exceptions
+ whilst waiting for the condition. Any exceptions not in this list
+ will be allowed to propagate, terminating the wait.
+
+ :param clock: Allows overriding the use of the runtime's
+ default time library.
+ """
+ self.session = session
+ self.timeout = timeout
+ self.interval = interval
+ self.exc_cls = raises
+ self.exc_msg = message
+ self.clock = clock
+
+ exceptions = []
+ if ignored_exceptions is not None:
+ if isinstance(ignored_exceptions, collections.abc.Iterable):
+ exceptions.extend(iter(ignored_exceptions))
+ else:
+ exceptions.append(ignored_exceptions)
+ self.exceptions = tuple(set(exceptions))
+
+ def until(self, condition):
+ """
+ This will repeatedly evaluate `condition` in anticipation
+ for a truthy return value, or the timeout to expire.
+
+ A condition that returns `None` or does not evaluate to
+ true will fully elapse its timeout before raising, unless
+ the `raises` keyword argument is `None`, in which case the
+ condition's return value is propagated unconditionally.
+
+ If an exception is raised in `condition` and it's not ignored,
+ this function will raise immediately. If the exception is
+ ignored it will be swallowed and polling will resume until
+ either the condition meets the return requirements or the
+ timeout duration is reached.
+
+ :param condition: A callable function whose return value will
+ be returned by this function.
+ """
+ rv = None
+ tb = None
+ start = self.clock.time()
+ end = start + self.timeout
+
+ while not self.clock.time() >= end:
+ try:
+ next = self.clock.time() + self.interval
+ rv = condition(self.session)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except self.exceptions:
+ _, _, tb = sys.exc_info()
+
+ # re-adjust the interval depending on how long
+ # the callback took to evaluate the condition
+ interval_new = max(next - self.clock.time(), 0)
+
+ if not rv:
+ self.clock.sleep(interval_new)
+ continue
+
+ if rv is not None:
+ return rv
+
+ self.clock.sleep(interval_new)
+
+ if self.exc_cls is not None:
+ elapsed = round((self.clock.time() - start), 1)
+ message = "Timed out after {} seconds".format(elapsed)
+ if self.exc_msg is not None:
+ message = "{} with message: {}".format(message, self.exc_msg)
+ raise self.exc_cls(message=message).with_traceback(tb)
+ else:
+ return rv
+
+
+class AsyncPoll(object):
+ """
+ An explicit conditional utility primitive for asynchronously polling
+ until a condition evaluates to something truthy.
+
+ A `Poll` instance defines the maximum amount of time to wait
+ for a condition, as well as the frequency with which to check
+ the condition. Furthermore, the user may configure the wait
+ to ignore specific types of exceptions whilst waiting, such as
+ `error.NoSuchElementException` when searching for an element
+ on the page.
+ """
+
+ def __init__(self,
+ session,
+ timeout=DEFAULT_TIMEOUT,
+ interval=DEFAULT_INTERVAL,
+ raises=error.TimeoutException,
+ message=None,
+ ignored_exceptions=None,
+ clock=None):
+ """
+ Configure the poller to have a custom timeout, interval,
+ and list of ignored exceptions. Optionally a different time
+ implementation than the one provided by the event loop
+ (`asyncio.get_event_loop()`) can also be provided.
+
+ Sample usage::
+
+ # Wait 30 seconds for window to open,
+ # checking for its presence once every 5 seconds.
+ from support.sync import AsyncPoll
+ wait = AsyncPoll(session, timeout=30, interval=5,
+ ignored_exceptions=error.NoSuchWindowException)
+ window = await wait.until(lambda s: s.switch_to_window(42))
+
+ :param session: The input value to be provided to conditions,
+ usually a `webdriver.Session` instance.
+
+ :param timeout: How long to wait for the evaluated condition
+ to become true.
+
+ :param interval: How often the condition should be evaluated.
+ In reality the interval may be greater as the cost of
+ evaluating the condition function. If that is not the case the
+ interval for the next condition function call is shortend to keep
+ the original interval sequence as best as possible.
+
+ :param raises: Optional exception to raise when poll elapses.
+ If not used, an `error.TimeoutException` is raised.
+ If it is `None`, no exception is raised on the poll elapsing.
+
+ :param message: An optional message to include in `raises`'s
+ message if the `until` condition times out.
+
+ :param ignored_exceptions: Ignore specific types of exceptions
+ whilst waiting for the condition. Any exceptions not in this list
+ will be allowed to propagate, terminating the wait.
+
+ :param clock: Allows overriding the use of the asyncio.get_event_loop()
+ default time implementation.
+ """
+ self.session = session
+ self.timeout = timeout
+ self.interval = interval
+ self.exc_cls = raises
+ self.exc_msg = message
+ self.clock = clock if clock is not None else asyncio.get_event_loop()
+
+ exceptions = []
+ if ignored_exceptions is not None:
+ if isinstance(ignored_exceptions, collections.abc.Iterable):
+ exceptions.extend(iter(ignored_exceptions))
+ else:
+ exceptions.append(ignored_exceptions)
+ self.exceptions = tuple(set(exceptions))
+
+ async def until(self, condition):
+ """
+ This will repeatedly evaluate `condition` in anticipation
+ for a truthy return value, or the timeout to expire.
+
+ A condition that returns `None` or does not evaluate to
+ true will fully elapse its timeout before raising, unless
+ the `raises` keyword argument is `None`, in which case the
+ condition's return value is propagated unconditionally.
+
+ If an exception is raised in `condition` and it's not ignored,
+ this function will raise immediately. If the exception is
+ ignored it will be swallowed and polling will resume until
+ either the condition meets the return requirements or the
+ timeout duration is reached.
+
+ :param condition: A callable function whose return value will
+ be returned by this function.
+ """
+ async def poll():
+ result = None
+ traceback = None
+ start = self.clock.time()
+ end = start + self.timeout
+
+ while not self.clock.time() >= end:
+ next = self.clock.time() + self.interval
+
+ try:
+ result = condition(self.session)
+ if inspect.isawaitable(result):
+ result = await result
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except self.exceptions:
+ _, _, traceback = sys.exc_info()
+
+ # re-adjust the interval depending on how long
+ # the callback took to evaluate the condition
+ interval_new = max(next - self.clock.time(), 0)
+
+ if result:
+ return result
+
+ await asyncio.sleep(interval_new)
+
+ if self.exc_cls is not None:
+ elapsed = round((self.clock.time() - start), 1)
+ message = f"Timed out after {elapsed} seconds"
+ if self.exc_msg is not None:
+ message = f"{message} with message: {self.exc_msg}"
+ raise self.exc_cls(message=message).with_traceback(traceback)
+ else:
+ return result
+
+ return await poll()