Skip to main content

script/event_loop/
webdriver_handlers.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::{HashMap, HashSet};
6use std::ffi::CString;
7use std::ptr::NonNull;
8
9use cookie::Cookie;
10use embedder_traits::{
11    CustomHandlersAutomationMode, JSValue, JavaScriptEvaluationError,
12    JavaScriptEvaluationResultSerializationError, WebDriverFrameId, WebDriverJSResult,
13    WebDriverLoadStatus,
14};
15use euclid::default::{Point2D, Rect, Size2D};
16use hyper_serde::Serde;
17use js::context::{JSContext, NoGC};
18use js::conversions::{FromJSValConvertible, jsstr_to_string};
19use js::jsapi::{HandleValueArray, JSITER_OWNONLY, JSType, PropertyDescriptor};
20use js::jsval::UndefinedValue;
21use js::realm::CurrentRealm;
22use js::rust::wrappers2::{
23    IsArgumentsObject, JS_CallFunctionName, JS_HasOwnProperty, JS_TypeOfValue,
24};
25use js::rust::{HandleObject, HandleValue, IdVector};
26use net_traits::CookieSource::{HTTP, NonHTTP};
27use net_traits::CoreResourceMsg::{DeleteCookie, DeleteCookies, GetCookiesForUrl, SetCookieForUrl};
28use script_bindings::codegen::GenericBindings::PermissionStatusBinding::{
29    PermissionName, PermissionState,
30};
31use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
32use script_bindings::conversions::is_array_like;
33use script_bindings::js::{
34    GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetProperty, JS_GetPropertyById,
35    JS_IsExceptionPending,
36};
37use script_bindings::num::Finite;
38use script_bindings::reflector::DomObject;
39use script_bindings::settings_stack::run_a_script;
40use servo_base::Epoch;
41use servo_base::generic_channel::{self, GenericOneshotSender, GenericSend, GenericSender};
42use servo_base::id::{BrowsingContextId, PipelineId};
43use servo_config::pref;
44use webdriver::command::SetPermissionState;
45use webdriver::error::ErrorStatus;
46
47use crate::DomTypeHolder;
48use crate::dom::Promise;
49use crate::dom::attr::is_boolean_attribute;
50use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
51use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
52use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
53use crate::dom::bindings::codegen::Bindings::ElementBinding::{
54    ElementMethods, ScrollIntoViewOptions, ScrollLogicalPosition,
55};
56use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
57use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
58use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
59use crate::dom::bindings::codegen::Bindings::HTMLOrSVGOrMathMLElementBinding::FocusOptions;
60use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods;
61use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
62use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
63use crate::dom::bindings::codegen::Bindings::WindowBinding::{
64    ScrollBehavior, ScrollOptions, WindowMethods,
65};
66use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializerMethods;
67use crate::dom::bindings::codegen::Bindings::XPathResultBinding::{
68    XPathResultConstants, XPathResultMethods,
69};
70use crate::dom::bindings::codegen::UnionTypes::BooleanOrScrollIntoViewOptions;
71use crate::dom::bindings::conversions::{
72    ConversionBehavior, ConversionResult, get_property, get_property_jsval, jsid_to_string,
73    root_from_object,
74};
75use crate::dom::bindings::error::{
76    Error, ErrorInfo, javascript_error_info_from_error_info, report_pending_exception,
77    throw_dom_exception,
78};
79use crate::dom::bindings::inheritance::Castable;
80use crate::dom::bindings::reflector::DomGlobal;
81use crate::dom::bindings::root::DomRoot;
82use crate::dom::bindings::str::DOMString;
83use crate::dom::document::Document;
84use crate::dom::domrect::DOMRect;
85use crate::dom::element::Element;
86use crate::dom::eventtarget::EventTarget;
87use crate::dom::globalscope::GlobalScope;
88use crate::dom::html::form_controls::htmlinputelement::HTMLInputElement;
89use crate::dom::html::form_controls::input_type::InputType;
90use crate::dom::html::htmlbodyelement::HTMLBodyElement;
91use crate::dom::html::htmldatalistelement::HTMLDataListElement;
92use crate::dom::html::htmlelement::HTMLElement;
93use crate::dom::html::htmlformelement::FormControl;
94use crate::dom::html::htmliframeelement::HTMLIFrameElement;
95use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement;
96use crate::dom::html::htmloptionelement::HTMLOptionElement;
97use crate::dom::html::htmlselectelement::HTMLSelectElement;
98use crate::dom::html::htmltextareaelement::HTMLTextAreaElement;
99use crate::dom::iterators::ShadowIncluding;
100use crate::dom::node::{Node, NodeTraits};
101use crate::dom::nodelist::NodeList;
102use crate::dom::promisenativehandler::Callback;
103use crate::dom::types::{PromiseNativeHandler, ShadowRoot};
104use crate::dom::validitystate::ValidationFlags;
105use crate::dom::window::Window;
106use crate::dom::xmlserializer::XMLSerializer;
107use crate::event_loop::document_collection::DocumentCollection;
108use crate::event_loop::script_thread::ScriptThread;
109use crate::realms::enter_auto_realm;
110
111/// <https://w3c.github.io/webdriver/#dfn-is-stale>
112fn is_stale(element: &Element) -> bool {
113    // An element is stale if its node document is not the active document
114    // or if it is not connected.
115    !element.owner_document().is_active() || !element.is_connected()
116}
117
118/// <https://w3c.github.io/webdriver/#dfn-is-detached>
119fn is_detached(shadow_root: &ShadowRoot) -> bool {
120    // A shadow root is detached if its node document is not the active document
121    // or if the element node referred to as its host is stale.
122    !shadow_root.owner_document().is_active() || is_stale(&shadow_root.Host())
123}
124
125/// <https://w3c.github.io/webdriver/#dfn-disabled>
126fn is_disabled(element: &Element) -> bool {
127    // Step 1. If element is an option element or element is an optgroup element
128    if element.is::<HTMLOptionElement>() || element.is::<HTMLOptGroupElement>() {
129        // Step 1.1. For each inclusive ancestor `ancestor` of element
130        let disabled = element
131            .upcast::<Node>()
132            .inclusive_ancestors(ShadowIncluding::No)
133            .any(|node| {
134                if node.is::<HTMLOptGroupElement>() || node.is::<HTMLSelectElement>() {
135                    // Step 1.1.1. If `ancestor` is an optgroup element or `ancestor` is a select element,
136                    // and `ancestor` is actually disabled, return true.
137                    node.downcast::<Element>().unwrap().is_actually_disabled()
138                } else {
139                    false
140                }
141            });
142
143        // Step 1.2
144        // The spec suggests that we immediately return false if the above is not true.
145        // However, it causes disabled option element to not be considered as disabled.
146        // Hence, here we also check if the element itself is actually disabled.
147        if disabled {
148            return true;
149        }
150    }
151    // Step 2. Return element is actually disabled.
152    element.is_actually_disabled()
153}
154
155pub(crate) fn handle_get_known_window(
156    documents: &DocumentCollection,
157    pipeline: PipelineId,
158    webview_id: String,
159    reply: GenericSender<Result<(), ErrorStatus>>,
160) {
161    if reply
162        .send(
163            documents
164                .find_window(pipeline)
165                .map_or(Err(ErrorStatus::NoSuchWindow), |window| {
166                    let window_proxy = window.window_proxy();
167                    // Step 3-4: Window must be top level browsing context.
168                    if window_proxy.browsing_context_id() != window_proxy.webview_id() ||
169                        window_proxy.webview_id().to_string() != webview_id
170                    {
171                        Err(ErrorStatus::NoSuchWindow)
172                    } else {
173                        Ok(())
174                    }
175                }),
176        )
177        .is_err()
178    {
179        error!("Webdriver get known window reply failed");
180    }
181}
182
183pub(crate) fn handle_get_known_shadow_root(
184    documents: &DocumentCollection,
185    pipeline: PipelineId,
186    shadow_root_id: String,
187    reply: GenericSender<Result<(), ErrorStatus>>,
188) {
189    let result = get_known_shadow_root(documents, pipeline, shadow_root_id).map(|_| ());
190    if reply.send(result).is_err() {
191        error!("Webdriver get known shadow root reply failed");
192    }
193}
194
195/// <https://w3c.github.io/webdriver/#dfn-get-a-known-shadow-root>
196fn get_known_shadow_root(
197    documents: &DocumentCollection,
198    pipeline: PipelineId,
199    node_id: String,
200) -> Result<DomRoot<ShadowRoot>, ErrorStatus> {
201    let doc = documents
202        .find_document(pipeline)
203        .ok_or(ErrorStatus::NoSuchWindow)?;
204    // Step 1. If not node reference is known with session, session's current browsing context,
205    // and reference return error with error code no such shadow root.
206    if !ScriptThread::has_node_id(pipeline, &node_id) {
207        return Err(ErrorStatus::NoSuchShadowRoot);
208    }
209
210    // Step 2. Let node be the result of get a node with session,
211    // session's current browsing context, and reference.
212    let node = find_node_by_unique_id_in_document(&doc, node_id);
213
214    // Step 3. If node is not null and node does not implement ShadowRoot
215    // return error with error code no such shadow root.
216    if let Some(ref node) = node &&
217        !node.is::<ShadowRoot>()
218    {
219        return Err(ErrorStatus::NoSuchShadowRoot);
220    }
221
222    // Step 4.1. If node is null return error with error code detached shadow root.
223    let Some(node) = node else {
224        return Err(ErrorStatus::DetachedShadowRoot);
225    };
226
227    // Step 4.2. If node is detached return error with error code detached shadow root.
228    // A shadow root is detached if its node document is not the active document
229    // or if the element node referred to as its host is stale.
230    let shadow_root = DomRoot::downcast::<ShadowRoot>(node).unwrap();
231    if is_detached(&shadow_root) {
232        return Err(ErrorStatus::DetachedShadowRoot);
233    }
234    // Step 5. Return success with data node.
235    Ok(shadow_root)
236}
237
238pub(crate) fn handle_get_known_element(
239    documents: &DocumentCollection,
240    pipeline: PipelineId,
241    element_id: String,
242    reply: GenericSender<Result<(), ErrorStatus>>,
243) {
244    let result = get_known_element(documents, pipeline, element_id).map(|_| ());
245    if reply.send(result).is_err() {
246        error!("Webdriver get known element reply failed");
247    }
248}
249
250/// <https://w3c.github.io/webdriver/#dfn-get-a-known-element>
251fn get_known_element(
252    documents: &DocumentCollection,
253    pipeline: PipelineId,
254    node_id: String,
255) -> Result<DomRoot<Element>, ErrorStatus> {
256    let doc = documents
257        .find_document(pipeline)
258        .ok_or(ErrorStatus::NoSuchWindow)?;
259    // Step 1. If not node reference is known with session, session's current browsing context,
260    // and reference return error with error code no such element.
261    if !ScriptThread::has_node_id(pipeline, &node_id) {
262        return Err(ErrorStatus::NoSuchElement);
263    }
264    // Step 2.Let node be the result of get a node with session,
265    // session's current browsing context, and reference.
266    let node = find_node_by_unique_id_in_document(&doc, node_id);
267
268    // Step 3. If node is not null and node does not implement Element
269    // return error with error code no such element.
270    if let Some(ref node) = node &&
271        !node.is::<Element>()
272    {
273        return Err(ErrorStatus::NoSuchElement);
274    }
275    // Step 4.1. If node is null return error with error code stale element reference.
276    let Some(node) = node else {
277        return Err(ErrorStatus::StaleElementReference);
278    };
279    // Step 4.2. If node is stale return error with error code stale element reference.
280    let element = DomRoot::downcast::<Element>(node).unwrap();
281    if is_stale(&element) {
282        return Err(ErrorStatus::StaleElementReference);
283    }
284    // Step 5. Return success with data node.
285    Ok(element)
286}
287
288// This is also used by `dom/window.rs`
289pub(crate) fn find_node_by_unique_id_in_document(
290    document: &Document,
291    node_id: String,
292) -> Option<DomRoot<Node>> {
293    let pipeline = document.window().pipeline_id();
294    document
295        .upcast::<Node>()
296        .traverse_preorder(ShadowIncluding::Yes)
297        .find(|node| node.unique_id(pipeline) == node_id)
298}
299
300/// <https://w3c.github.io/webdriver/#dfn-link-text-selector>
301fn matching_links<'a>(
302    cx: &'a NoGC,
303    links: &'a NodeList,
304    link_text: String,
305    partial: bool,
306) -> impl Iterator<Item = String> + 'a {
307    links
308        .iter(cx)
309        .filter(move |node| {
310            let content = node
311                .downcast::<HTMLElement>()
312                .map(|element| element.InnerText())
313                .map_or(String::new(), String::from)
314                .trim()
315                .to_owned();
316            if partial {
317                content.contains(&link_text)
318            } else {
319                content == link_text
320            }
321        })
322        .map(|node| node.unique_id(node.owner_doc().window().pipeline_id()))
323}
324
325fn all_matching_links(
326    cx: &mut JSContext,
327    root_node: &Node,
328    link_text: String,
329    partial: bool,
330) -> Result<Vec<String>, ErrorStatus> {
331    // <https://w3c.github.io/webdriver/#dfn-find>
332    // Step 7.2. If a DOMException, SyntaxError, XPathException, or other error occurs
333    // during the execution of the element location strategy, return error invalid selector.
334    root_node
335        .query_selector_all(cx, DOMString::from_static("a"))
336        .map_err(|_| ErrorStatus::InvalidSelector)
337        .map(|nodes| matching_links(cx, &nodes, link_text, partial).collect())
338}
339
340#[expect(unsafe_code)]
341fn object_has_to_json_property(
342    cx: &mut JSContext,
343    global_scope: &GlobalScope,
344    object: HandleObject,
345) -> bool {
346    let name = CString::new("toJSON").unwrap();
347    let mut found = false;
348    if unsafe { JS_HasOwnProperty(cx, object, name.as_ptr(), &mut found) } && found {
349        rooted!(&in(cx) let mut value = UndefinedValue());
350        let result = JS_GetProperty(cx, object, &name, value.handle_mut());
351        if !result {
352            throw_dom_exception(cx, global_scope, Error::JSFailed);
353            false
354        } else {
355            result && unsafe { JS_TypeOfValue(cx, value.handle()) } == JSType::JSTYPE_FUNCTION
356        }
357    } else if JS_IsExceptionPending(cx) {
358        throw_dom_exception(cx, global_scope, Error::JSFailed);
359        false
360    } else {
361        false
362    }
363}
364
365#[expect(unsafe_code)]
366/// <https://w3c.github.io/webdriver/#dfn-collection>
367fn is_arguments_object(object: HandleObject) -> bool {
368    unsafe { IsArgumentsObject(object) }
369}
370
371#[derive(Clone, Eq, Hash, PartialEq)]
372struct HashableJSVal(u64);
373
374impl From<HandleValue<'_>> for HashableJSVal {
375    fn from(v: HandleValue<'_>) -> HashableJSVal {
376        HashableJSVal(v.get().asBits_)
377    }
378}
379
380/// <https://w3c.github.io/webdriver/#dfn-json-clone>
381pub(crate) fn jsval_to_webdriver(
382    cx: &mut CurrentRealm,
383    global_scope: &GlobalScope,
384    val: HandleValue,
385) -> WebDriverJSResult {
386    run_a_script::<DomTypeHolder, _, _>(cx, global_scope, |cx| {
387        let mut seen = HashSet::new();
388        let result = jsval_to_webdriver_inner(cx, global_scope, val, &mut seen);
389
390        if result.is_err() {
391            report_pending_exception(cx);
392        }
393        result
394    })
395}
396
397#[expect(unsafe_code)]
398/// <https://w3c.github.io/webdriver/#dfn-internal-json-clone>
399fn jsval_to_webdriver_inner(
400    cx: &mut CurrentRealm,
401    global_scope: &GlobalScope,
402    val: HandleValue,
403    seen: &mut HashSet<HashableJSVal>,
404) -> WebDriverJSResult {
405    if val.get().is_undefined() {
406        Ok(JSValue::Undefined)
407    } else if val.get().is_null() {
408        Ok(JSValue::Null)
409    } else if val.get().is_boolean() {
410        Ok(JSValue::Boolean(val.get().to_boolean()))
411    } else if val.get().is_number() {
412        Ok(JSValue::Number(val.to_number()))
413    } else if val.get().is_string() {
414        let string = NonNull::new(val.to_string()).expect("Should have a non-Null String");
415        let string = unsafe { jsstr_to_string(cx, string) };
416        Ok(JSValue::String(string))
417    } else if val.get().is_object() {
418        rooted!(&in(cx) let object = match FromJSValConvertible::from_jsval(cx, val, ()).unwrap() {
419            ConversionResult::Success(object) => object,
420            _ => unreachable!(),
421        });
422
423        if let Ok(element) = unsafe { root_from_object::<Element>(cx, *object) } {
424            // If the element is stale, return error with error code stale element reference.
425            if is_stale(&element) {
426                Err(JavaScriptEvaluationError::SerializationError(
427                    JavaScriptEvaluationResultSerializationError::StaleElementReference,
428                ))
429            } else {
430                Ok(JSValue::Element(
431                    element
432                        .upcast::<Node>()
433                        .unique_id(element.owner_window().pipeline_id()),
434                ))
435            }
436        } else if let Ok(shadow_root) = unsafe { root_from_object::<ShadowRoot>(cx, *object) } {
437            // If the shadow root is detached, return error with error code detached shadow root.
438            if is_detached(&shadow_root) {
439                Err(JavaScriptEvaluationError::SerializationError(
440                    JavaScriptEvaluationResultSerializationError::DetachedShadowRoot,
441                ))
442            } else {
443                Ok(JSValue::ShadowRoot(
444                    shadow_root
445                        .upcast::<Node>()
446                        .unique_id(shadow_root.owner_window().pipeline_id()),
447                ))
448            }
449        } else if let Ok(window) = unsafe { root_from_object::<Window>(cx, *object) } {
450            let window_proxy = window.window_proxy();
451            if window_proxy.is_browsing_context_discarded() {
452                Err(JavaScriptEvaluationError::SerializationError(
453                    JavaScriptEvaluationResultSerializationError::StaleElementReference,
454                ))
455            } else if window_proxy.browsing_context_id() == window_proxy.webview_id() {
456                Ok(JSValue::Window(window.webview_id().to_string()))
457            } else {
458                Ok(JSValue::Frame(
459                    window_proxy.browsing_context_id().to_string(),
460                ))
461            }
462        } else if object_has_to_json_property(cx, global_scope, object.handle()) {
463            let name = CString::new("toJSON").unwrap();
464            rooted!(&in(cx) let mut value = UndefinedValue());
465            let call_result = unsafe {
466                JS_CallFunctionName(
467                    cx,
468                    object.handle(),
469                    name.as_ptr(),
470                    &HandleValueArray::empty(),
471                    value.handle_mut(),
472                )
473            };
474
475            if call_result {
476                Ok(jsval_to_webdriver_inner(
477                    cx,
478                    global_scope,
479                    value.handle(),
480                    seen,
481                )?)
482            } else {
483                throw_dom_exception(cx, global_scope, Error::JSFailed);
484                Err(JavaScriptEvaluationError::SerializationError(
485                    JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
486                ))
487            }
488        } else {
489            clone_an_object(cx, global_scope, val, seen, object.handle())
490        }
491    } else {
492        Err(JavaScriptEvaluationError::SerializationError(
493            JavaScriptEvaluationResultSerializationError::UnknownType,
494        ))
495    }
496}
497
498/// <https://w3c.github.io/webdriver/#dfn-clone-an-object>
499fn clone_an_object(
500    cx: &mut CurrentRealm,
501    global_scope: &GlobalScope,
502    val: HandleValue,
503    seen: &mut HashSet<HashableJSVal>,
504    object_handle: HandleObject,
505) -> WebDriverJSResult {
506    let hashable = val.into();
507    // Step 1. If value is in `seen`, return error with error code javascript error.
508    if seen.contains(&hashable) {
509        return Err(JavaScriptEvaluationError::SerializationError(
510            JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
511        ));
512    }
513    // Step 2. Append value to `seen`.
514    seen.insert(hashable.clone());
515
516    let return_val = if is_array_like::<crate::DomTypeHolder>(cx, val) ||
517        is_arguments_object(object_handle)
518    {
519        let mut result: Vec<JSValue> = Vec::new();
520
521        let get_property_result =
522            get_property::<u32>(cx, object_handle, c"length", ConversionBehavior::Default);
523        let length = match get_property_result {
524            Ok(length) => match length {
525                Some(length) => length,
526                _ => {
527                    return Err(JavaScriptEvaluationError::SerializationError(
528                        JavaScriptEvaluationResultSerializationError::UnknownType,
529                    ));
530                },
531            },
532            Err(error) => {
533                throw_dom_exception(cx, global_scope, error);
534                return Err(JavaScriptEvaluationError::SerializationError(
535                    JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
536                ));
537            },
538        };
539        // Step 4. For each enumerable property in value, run the following substeps:
540        for i in 0..length {
541            rooted!(&in(cx) let mut item = UndefinedValue());
542            let cname = CString::new(i.to_string()).unwrap();
543            let get_property_result =
544                get_property_jsval(cx, object_handle, &cname, item.handle_mut());
545            match get_property_result {
546                Ok(_) => {
547                    let converted_item =
548                        jsval_to_webdriver_inner(cx, global_scope, item.handle(), seen)?;
549
550                    result.push(converted_item);
551                },
552                Err(error) => {
553                    throw_dom_exception(cx, global_scope, error);
554                    return Err(JavaScriptEvaluationError::SerializationError(
555                        JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
556                    ));
557                },
558            }
559        }
560        Ok(JSValue::Array(result))
561    } else {
562        let mut result = HashMap::new();
563
564        let mut ids = IdVector::new(cx);
565        let succeeded = GetPropertyKeys(cx, object_handle, JSITER_OWNONLY, ids.handle_mut());
566        if !succeeded {
567            return Err(JavaScriptEvaluationError::SerializationError(
568                JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
569            ));
570        }
571        for id in ids.iter() {
572            rooted!(&in(cx) let id = *id);
573            rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
574
575            let mut is_none = false;
576            let succeeded = JS_GetOwnPropertyDescriptorById(
577                cx,
578                object_handle,
579                id.handle(),
580                desc.handle_mut(),
581                &mut is_none,
582            );
583            if !succeeded {
584                return Err(JavaScriptEvaluationError::SerializationError(
585                    JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
586                ));
587            }
588
589            rooted!(&in(cx) let mut property = UndefinedValue());
590            let succeeded =
591                JS_GetPropertyById(cx, object_handle, id.handle(), property.handle_mut());
592            if !succeeded {
593                return Err(JavaScriptEvaluationError::SerializationError(
594                    JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
595                ));
596            }
597
598            if !property.is_undefined() {
599                let name = jsid_to_string(cx, id.handle());
600                let Some(name) = name else {
601                    return Err(JavaScriptEvaluationError::SerializationError(
602                        JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
603                    ));
604                };
605
606                let value = jsval_to_webdriver_inner(cx, global_scope, property.handle(), seen)?;
607                result.insert(name.into(), value);
608            }
609        }
610        Ok(JSValue::Object(result))
611    };
612    // Step 5. Remove the last element of `seen`.
613    seen.remove(&hashable);
614    // Step 6. Return success with data `result`.
615    return_val
616}
617
618#[derive(MallocSizeOf, JSTraceable)]
619struct WebDriverExecuteScriptFulfillmentHandler {
620    #[no_trace]
621    reply_sender: GenericSender<WebDriverJSResult>,
622}
623
624impl Callback for WebDriverExecuteScriptFulfillmentHandler {
625    fn callback(&self, cx: &mut CurrentRealm, return_value: HandleValue) {
626        let global_scope = GlobalScope::from_current_realm(cx);
627        let result = jsval_to_webdriver(cx, &global_scope, return_value);
628        let _ = self.reply_sender.send(result);
629    }
630}
631
632#[derive(MallocSizeOf, JSTraceable)]
633struct WebDriverExecuteScriptRejectionHandler {
634    #[no_trace]
635    reply_sender: GenericSender<WebDriverJSResult>,
636}
637
638impl Callback for WebDriverExecuteScriptRejectionHandler {
639    fn callback(&self, cx: &mut CurrentRealm, return_value: HandleValue) {
640        let error_info = ErrorInfo::from_value(cx, return_value);
641        let _ = self
642            .reply_sender
643            .send(Err(JavaScriptEvaluationError::EvaluationFailure(Some(
644                javascript_error_info_from_error_info(cx, &error_info, return_value),
645            ))));
646    }
647}
648
649pub(crate) fn handle_execute_script(
650    cx: &mut JSContext,
651    window: Option<DomRoot<Window>>,
652    eval: String,
653    reply_sender: GenericSender<WebDriverJSResult>,
654) {
655    let Some(window) = window else {
656        reply_sender
657            .send(Err(JavaScriptEvaluationError::DocumentNotFound))
658            .unwrap_or_else(|error| {
659                error!("ExecuteAsyncScript Failed to send reply: {error}");
660            });
661        return;
662    };
663
664    let global_scope = window.as_global_scope();
665    let mut realm = enter_auto_realm(cx, global_scope);
666    let mut realm = realm.current_realm();
667    let cx = &mut realm;
668
669    rooted!(&in(cx) let mut return_value = UndefinedValue());
670    if let Err(error) = global_scope.evaluate_js_on_global(
671        cx,
672        eval.into(),
673        "",
674        None, // No known `introductionType` for JS code from WebDriver
675        Some(return_value.handle_mut()),
676    ) {
677        reply_sender.send(Err(error)).unwrap_or_else(|error| {
678            error!("ExecuteAsyncScript Failed to send reply: {error}");
679        });
680        return;
681    }
682
683    let promise = Promise::new_resolved(cx, global_scope, return_value.handle());
684    let fulfillment_handler = WebDriverExecuteScriptFulfillmentHandler {
685        reply_sender: reply_sender.clone(),
686    };
687    let rejection_handler = WebDriverExecuteScriptRejectionHandler { reply_sender };
688
689    let handler = PromiseNativeHandler::new(
690        cx,
691        global_scope,
692        Some(Box::new(fulfillment_handler)),
693        Some(Box::new(rejection_handler)),
694    );
695    promise.append_native_handler(cx, &handler);
696}
697
698/// Get BrowsingContextId for <https://w3c.github.io/webdriver/#switch-to-parent-frame>
699pub(crate) fn handle_get_parent_frame_id(
700    documents: &DocumentCollection,
701    pipeline: PipelineId,
702    reply: GenericSender<Result<BrowsingContextId, ErrorStatus>>,
703) {
704    // Step 2. If session's current parent browsing context is no longer open,
705    // return error with error code no such window.
706    reply
707        .send(
708            documents
709                .find_window(pipeline)
710                .and_then(|window| {
711                    window
712                        .window_proxy()
713                        .parent()
714                        .map(|parent| parent.browsing_context_id())
715                })
716                .ok_or(ErrorStatus::NoSuchWindow),
717        )
718        .unwrap();
719}
720
721/// Get the BrowsingContextId for <https://w3c.github.io/webdriver/#dfn-switch-to-frame>
722pub(crate) fn handle_get_browsing_context_id(
723    documents: &DocumentCollection,
724    pipeline: PipelineId,
725    webdriver_frame_id: WebDriverFrameId,
726    reply: GenericSender<Result<BrowsingContextId, ErrorStatus>>,
727) {
728    reply
729        .send(match webdriver_frame_id {
730            WebDriverFrameId::Short(id) => {
731                // Step 5. If id is not a supported property index of window,
732                // return error with error code no such frame.
733                documents
734                    .find_document(pipeline)
735                    .ok_or(ErrorStatus::NoSuchWindow)
736                    .and_then(|document| {
737                        document
738                            .iframes()
739                            .iter()
740                            .nth(id as usize)
741                            .and_then(|iframe| iframe.browsing_context_id())
742                            .ok_or(ErrorStatus::NoSuchFrame)
743                    })
744            },
745            WebDriverFrameId::Element(element_id) => {
746                get_known_element(documents, pipeline, element_id).and_then(|element| {
747                    element
748                        .downcast::<HTMLIFrameElement>()
749                        .and_then(|element| element.browsing_context_id())
750                        .ok_or(ErrorStatus::NoSuchFrame)
751                })
752            },
753        })
754        .unwrap();
755}
756
757/// <https://w3c.github.io/webdriver/#dfn-center-point>
758fn get_element_in_view_center_point(cx: &mut JSContext, element: &Element) -> Option<Point2D<i64>> {
759    let doc = element.owner_document();
760    // Step 1: Let rectangle be the first element of the DOMRect sequence
761    // returned by calling getClientRects() on element.
762    element.GetClientRects(cx).first().map(|rectangle| {
763        let x = rectangle.X();
764        let y = rectangle.Y();
765        let width = rectangle.Width();
766        let height = rectangle.Height();
767        debug!(
768            "get_element_in_view_center_point: Element rectangle at \
769            (x: {x}, y: {y}, width: {width}, height: {height})",
770        );
771        let window = doc.window();
772        // Steps 2. Let left be max(0, min(x coordinate, x coordinate + width dimension)).
773        let left = (x.min(x + width)).max(0.0);
774        // Step 3. Let right be min(innerWidth, max(x coordinate, x coordinate + width dimension)).
775        let right = f64::min(window.InnerWidth() as f64, x.max(x + width));
776        // Step 4. Let top be max(0, min(y coordinate, y coordinate + height dimension)).
777        let top = (y.min(y + height)).max(0.0);
778        // Step 5. Let bottom be
779        // min(innerHeight, max(y coordinate, y coordinate + height dimension)).
780        let bottom = f64::min(window.InnerHeight() as f64, y.max(y + height));
781        debug!(
782            "get_element_in_view_center_point: Computed rectangle is \
783            (left: {left}, right: {right}, top: {top}, bottom: {bottom})",
784        );
785        // Step 6. Let x be floor((left + right) ÷ 2.0).
786        let center_x = ((left + right) / 2.0).floor() as i64;
787        // Step 7. Let y be floor((top + bottom) ÷ 2.0).
788        let center_y = ((top + bottom) / 2.0).floor() as i64;
789
790        debug!(
791            "get_element_in_view_center_point: Element center point at ({center_x}, {center_y})",
792        );
793        // Step 8
794        Point2D::new(center_x, center_y)
795    })
796}
797
798pub(crate) fn handle_get_element_in_view_center_point(
799    cx: &mut JSContext,
800    documents: &DocumentCollection,
801    pipeline: PipelineId,
802    element_id: String,
803    reply: GenericOneshotSender<Result<Option<(i64, i64)>, ErrorStatus>>,
804) {
805    reply
806        .send(
807            get_known_element(documents, pipeline, element_id).map(|element| {
808                get_element_in_view_center_point(cx, &element).map(|point| (point.x, point.y))
809            }),
810        )
811        .unwrap();
812}
813
814fn retrieve_document_and_check_root_existence(
815    documents: &DocumentCollection,
816    pipeline: PipelineId,
817) -> Result<DomRoot<Document>, ErrorStatus> {
818    let document = documents
819        .find_document(pipeline)
820        .ok_or(ErrorStatus::NoSuchWindow)?;
821
822    // <https://w3c.github.io/webdriver/#find-element>
823    // <https://w3c.github.io/webdriver/#find-elements>
824    // Step 7 - 8. If current browsing context's document element is null,
825    // return error with error code no such element.
826    if document.GetDocumentElement().is_none() {
827        Err(ErrorStatus::NoSuchElement)
828    } else {
829        Ok(document)
830    }
831}
832
833pub(crate) fn handle_find_elements_css_selector(
834    cx: &mut JSContext,
835    documents: &DocumentCollection,
836    pipeline: PipelineId,
837    selector: String,
838    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
839) {
840    match retrieve_document_and_check_root_existence(documents, pipeline) {
841        Ok(document) => reply
842            .send(
843                document
844                    .QuerySelectorAll(cx, DOMString::from(selector))
845                    .map_err(|_| ErrorStatus::InvalidSelector)
846                    .map(|nodes| {
847                        nodes
848                            .iter(cx)
849                            .map(|x| x.upcast::<Node>().unique_id(pipeline))
850                            .collect()
851                    }),
852            )
853            .unwrap(),
854        Err(error) => reply.send(Err(error)).unwrap(),
855    }
856}
857
858pub(crate) fn handle_find_elements_link_text(
859    cx: &mut JSContext,
860    documents: &DocumentCollection,
861    pipeline: PipelineId,
862    selector: String,
863    partial: bool,
864    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
865) {
866    match retrieve_document_and_check_root_existence(documents, pipeline) {
867        Ok(document) => reply
868            .send(all_matching_links(
869                cx,
870                document.upcast::<Node>(),
871                selector,
872                partial,
873            ))
874            .unwrap(),
875        Err(error) => reply.send(Err(error)).unwrap(),
876    }
877}
878
879pub(crate) fn handle_find_elements_tag_name(
880    cx: &mut JSContext,
881    documents: &DocumentCollection,
882    pipeline: PipelineId,
883    selector: String,
884    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
885) {
886    match retrieve_document_and_check_root_existence(documents, pipeline) {
887        Ok(document) => reply
888            .send(Ok(document
889                .GetElementsByTagName(cx, DOMString::from(selector))
890                .elements_iter(cx.no_gc())
891                .map(|x| x.upcast::<Node>().unique_id(pipeline))
892                .collect::<Vec<String>>()))
893            .unwrap(),
894        Err(error) => reply.send(Err(error)).unwrap(),
895    }
896}
897
898/// <https://w3c.github.io/webdriver/#xpath>
899fn find_elements_xpath_strategy(
900    cx: &mut JSContext,
901    document: &Document,
902    start_node: &Node,
903    selector: String,
904    pipeline: PipelineId,
905) -> Result<Vec<String>, ErrorStatus> {
906    // Step 1. Let evaluateResult be the result of calling evaluate,
907    // with arguments selector, start node, null, ORDERED_NODE_SNAPSHOT_TYPE, and null.
908
909    // A snapshot is used to promote operation atomicity.
910    let evaluate_result = match document.Evaluate(
911        cx,
912        DOMString::from(selector),
913        start_node,
914        None,
915        XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE,
916        None,
917    ) {
918        Ok(res) => res,
919        Err(_) => return Err(ErrorStatus::InvalidSelector),
920    };
921    // Step 2. Let index be 0. (Handled altogether in Step 5.)
922
923    // Step 3: Let length be the result of getting the property "snapshotLength"
924    // from evaluateResult.
925
926    let length = match evaluate_result.GetSnapshotLength() {
927        Ok(len) => len,
928        Err(_) => return Err(ErrorStatus::InvalidSelector),
929    };
930
931    // Step 4: Prepare result vector
932    let mut result = Vec::new();
933
934    // Step 5: Repeat, while index is less than length:
935    for index in 0..length {
936        // Step 5.1. Let node be the result of calling snapshotItem with
937        // evaluateResult as this and index as the argument.
938        let node = match evaluate_result.SnapshotItem(index) {
939            Ok(node) => node.expect(
940                "Node should always exist as ORDERED_NODE_SNAPSHOT_TYPE \
941                                gives static result and we verified the length!",
942            ),
943            Err(_) => return Err(ErrorStatus::InvalidSelector),
944        };
945
946        // Step 5.2. If node is not an element return an error with error code invalid selector.
947        if !node.is::<Element>() {
948            return Err(ErrorStatus::InvalidSelector);
949        }
950
951        // Step 5.3. Append node to result.
952        result.push(node.unique_id(pipeline));
953    }
954    // Step 6. Return success with data result.
955    Ok(result)
956}
957
958pub(crate) fn handle_find_elements_xpath_selector(
959    cx: &mut JSContext,
960    documents: &DocumentCollection,
961    pipeline: PipelineId,
962    selector: String,
963    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
964) {
965    match retrieve_document_and_check_root_existence(documents, pipeline) {
966        Ok(document) => reply
967            .send(find_elements_xpath_strategy(
968                cx,
969                &document,
970                document.upcast::<Node>(),
971                selector,
972                pipeline,
973            ))
974            .unwrap(),
975        Err(error) => reply.send(Err(error)).unwrap(),
976    }
977}
978
979pub(crate) fn handle_find_element_elements_css_selector(
980    cx: &mut JSContext,
981    documents: &DocumentCollection,
982    pipeline: PipelineId,
983    element_id: String,
984    selector: String,
985    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
986) {
987    reply
988        .send(
989            get_known_element(documents, pipeline, element_id).and_then(|element| {
990                element
991                    .upcast::<Node>()
992                    .query_selector_all(cx, DOMString::from(selector))
993                    .map_err(|_| ErrorStatus::InvalidSelector)
994                    .map(|nodes| {
995                        nodes
996                            .iter(cx)
997                            .map(|x| x.upcast::<Node>().unique_id(pipeline))
998                            .collect()
999                    })
1000            }),
1001        )
1002        .unwrap();
1003}
1004
1005pub(crate) fn handle_find_element_elements_link_text(
1006    cx: &mut JSContext,
1007    documents: &DocumentCollection,
1008    pipeline: PipelineId,
1009    element_id: String,
1010    selector: String,
1011    partial: bool,
1012    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1013) {
1014    reply
1015        .send(
1016            get_known_element(documents, pipeline, element_id).and_then(|element| {
1017                all_matching_links(cx, element.upcast::<Node>(), selector.clone(), partial)
1018            }),
1019        )
1020        .unwrap();
1021}
1022
1023pub(crate) fn handle_find_element_elements_tag_name(
1024    cx: &mut JSContext,
1025    documents: &DocumentCollection,
1026    pipeline: PipelineId,
1027    element_id: String,
1028    selector: String,
1029    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1030) {
1031    reply
1032        .send(
1033            get_known_element(documents, pipeline, element_id).map(|element| {
1034                element
1035                    .GetElementsByTagName(cx, DOMString::from(selector))
1036                    .elements_iter(cx.no_gc())
1037                    .map(|x| x.upcast::<Node>().unique_id(pipeline))
1038                    .collect::<Vec<String>>()
1039            }),
1040        )
1041        .unwrap();
1042}
1043
1044pub(crate) fn handle_find_element_elements_xpath_selector(
1045    cx: &mut JSContext,
1046    documents: &DocumentCollection,
1047    pipeline: PipelineId,
1048    element_id: String,
1049    selector: String,
1050    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1051) {
1052    reply
1053        .send(
1054            get_known_element(documents, pipeline, element_id).and_then(|element| {
1055                find_elements_xpath_strategy(
1056                    cx,
1057                    &documents
1058                        .find_document(pipeline)
1059                        .expect("Document existence guaranteed by `get_known_element`"),
1060                    element.upcast::<Node>(),
1061                    selector,
1062                    pipeline,
1063                )
1064            }),
1065        )
1066        .unwrap();
1067}
1068
1069/// <https://w3c.github.io/webdriver/#find-elements-from-shadow-root>
1070pub(crate) fn handle_find_shadow_elements_css_selector(
1071    cx: &mut JSContext,
1072    documents: &DocumentCollection,
1073    pipeline: PipelineId,
1074    shadow_root_id: String,
1075    selector: String,
1076    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1077) {
1078    reply
1079        .send(
1080            get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1081                shadow_root
1082                    .upcast::<Node>()
1083                    .query_selector_all(cx, DOMString::from(selector))
1084                    .map_err(|_| ErrorStatus::InvalidSelector)
1085                    .map(|nodes| {
1086                        nodes
1087                            .iter(cx)
1088                            .map(|x| x.upcast::<Node>().unique_id(pipeline))
1089                            .collect()
1090                    })
1091            }),
1092        )
1093        .unwrap();
1094}
1095
1096pub(crate) fn handle_find_shadow_elements_link_text(
1097    cx: &mut JSContext,
1098    documents: &DocumentCollection,
1099    pipeline: PipelineId,
1100    shadow_root_id: String,
1101    selector: String,
1102    partial: bool,
1103    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1104) {
1105    reply
1106        .send(
1107            get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1108                all_matching_links(cx, shadow_root.upcast::<Node>(), selector.clone(), partial)
1109            }),
1110        )
1111        .unwrap();
1112}
1113
1114pub(crate) fn handle_find_shadow_elements_tag_name(
1115    cx: &mut JSContext,
1116    documents: &DocumentCollection,
1117    pipeline: PipelineId,
1118    shadow_root_id: String,
1119    selector: String,
1120    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1121) {
1122    // According to spec, we should use `getElementsByTagName`. But it is wrong, as only
1123    // Document and Element implement this method. So we use `querySelectorAll` instead.
1124    // But we should not return InvalidSelector error if the selector is not valid,
1125    // as `getElementsByTagName` won't.
1126    // See https://github.com/w3c/webdriver/issues/1903
1127    reply
1128        .send(
1129            get_known_shadow_root(documents, pipeline, shadow_root_id).map(|shadow_root| {
1130                shadow_root
1131                    .upcast::<Node>()
1132                    .query_selector_all(cx, DOMString::from(selector))
1133                    .map(|nodes| {
1134                        nodes
1135                            .iter(cx)
1136                            .map(|x| x.upcast::<Node>().unique_id(pipeline))
1137                            .collect()
1138                    })
1139                    .unwrap_or_default()
1140            }),
1141        )
1142        .unwrap();
1143}
1144
1145pub(crate) fn handle_find_shadow_elements_xpath_selector(
1146    cx: &mut JSContext,
1147    documents: &DocumentCollection,
1148    pipeline: PipelineId,
1149    shadow_root_id: String,
1150    selector: String,
1151    reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1152) {
1153    reply
1154        .send(
1155            get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1156                find_elements_xpath_strategy(
1157                    cx,
1158                    &documents
1159                        .find_document(pipeline)
1160                        .expect("Document existence guaranteed by `get_known_shadow_root`"),
1161                    shadow_root.upcast::<Node>(),
1162                    selector,
1163                    pipeline,
1164                )
1165            }),
1166        )
1167        .unwrap();
1168}
1169
1170/// <https://www.w3.org/TR/webdriver2/#dfn-get-element-shadow-root>
1171pub(crate) fn handle_get_element_shadow_root(
1172    documents: &DocumentCollection,
1173    pipeline: PipelineId,
1174    element_id: String,
1175    reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1176) {
1177    reply
1178        .send(
1179            get_known_element(documents, pipeline, element_id).map(|element| {
1180                element
1181                    .shadow_root()
1182                    .map(|x| x.upcast::<Node>().unique_id(pipeline))
1183            }),
1184        )
1185        .unwrap();
1186}
1187
1188impl Element {
1189    /// <https://w3c.github.io/webdriver/#dfn-keyboard-interactable>
1190    fn is_keyboard_interactable(&self, no_gc: &NoGC) -> bool {
1191        self.is_focusable_area(no_gc) || self.is::<HTMLBodyElement>() || self.is_document_element()
1192    }
1193}
1194
1195fn handle_send_keys_file(
1196    cx: &mut JSContext,
1197    file_input: &HTMLInputElement,
1198    text: &str,
1199    reply_sender: GenericSender<Result<bool, ErrorStatus>>,
1200) {
1201    // Step 1. Let files be the result of splitting text
1202    // on the newline (\n) character.
1203    //
1204    // Be sure to also remove empty strings, as "" always splits to a single string.
1205    let files: Vec<DOMString> = text
1206        .split("\n")
1207        .filter_map(|string| {
1208            if string.is_empty() {
1209                None
1210            } else {
1211                Some(string.into())
1212            }
1213        })
1214        .collect();
1215
1216    // Step 2. If files is of 0 length, return ErrorStatus::InvalidArgument.
1217    if files.is_empty() {
1218        let _ = reply_sender.send(Err(ErrorStatus::InvalidArgument));
1219        return;
1220    }
1221
1222    // Step 3. Let multiple equal the result of calling hasAttribute() with "multiple" on
1223    // element. Step 4. If multiple is false and the length of files is not equal to 1,
1224    // return ErrorStatus::InvalidArgument.
1225    if !file_input.Multiple() && files.len() > 1 {
1226        let _ = reply_sender.send(Err(ErrorStatus::InvalidArgument));
1227        return;
1228    }
1229
1230    // Step 5. Return ErrorStatus::InvalidArgument if the files does not exist.
1231    // Step 6. Set the selected files on the input event.
1232    // TODO: If multiple is true files are be appended to element's selected files.
1233    // Step 7. Fire input and change event (should already be fired in `htmlinputelement.rs`)
1234    // Step 8. Return success with data null.
1235    //
1236    // Do not reply to the response yet, as we are waiting for the files to arrive
1237    // asynchronously.
1238    file_input.select_files_for_webdriver(cx, files, reply_sender);
1239}
1240
1241/// We have verify previously that input element is not textual.
1242fn handle_send_keys_non_typeable(
1243    cx: &mut JSContext,
1244    input_element: &HTMLInputElement,
1245    text: &str,
1246) -> Result<bool, ErrorStatus> {
1247    // Step 1. If element does not have an own property named value,
1248    // Return ErrorStatus::ElementNotInteractable.
1249    // Currently, we only support HTMLInputElement for non-typeable
1250    // form controls. Hence, it should always have value property.
1251
1252    // Step 2. If element is not mutable, return ErrorStatus::ElementNotInteractable.
1253    if !input_element.is_mutable() {
1254        return Err(ErrorStatus::ElementNotInteractable);
1255    }
1256
1257    // Step 3. Set a property value to text on element.
1258    if let Err(error) = input_element.SetValue(cx, text.into()) {
1259        error!(
1260            "Failed to set value on non-typeable input element: {:?}",
1261            error
1262        );
1263        return Err(ErrorStatus::UnknownError);
1264    }
1265
1266    // Step 4. If element is suffering from bad input, return ErrorStatus::InvalidArgument.
1267    if input_element
1268        .Validity(cx)
1269        .invalid_flags()
1270        .contains(ValidationFlags::BAD_INPUT)
1271    {
1272        return Err(ErrorStatus::InvalidArgument);
1273    }
1274
1275    // Step 5. Return success with data null.
1276    // This is done in `webdriver_server:lib.rs`
1277    Ok(false)
1278}
1279
1280/// Implementing step 5 - 7, plus part of step 8 of "Element Send Keys"
1281/// where element is input element in the file upload state.
1282/// This function will send a boolean back to webdriver_server,
1283/// indicating whether the dispatching of the key and
1284/// composition event is still needed or not.
1285pub(crate) fn handle_will_send_keys(
1286    cx: &mut JSContext,
1287    documents: &DocumentCollection,
1288    pipeline: PipelineId,
1289    element_id: String,
1290    text: String,
1291    strict_file_interactability: bool,
1292    reply: GenericSender<Result<bool, ErrorStatus>>,
1293) {
1294    // Set 5. Let element be the result of trying to get a known element.
1295    let element = match get_known_element(documents, pipeline, element_id) {
1296        Ok(element) => element,
1297        Err(error) => {
1298            let _ = reply.send(Err(error));
1299            return;
1300        },
1301    };
1302
1303    let input_element = element.downcast::<HTMLInputElement>();
1304    let mut element_has_focus = false;
1305
1306    // Step 6: Let file be true if element is input element
1307    // in the file upload state, or false otherwise
1308    let is_file_input =
1309        input_element.is_some_and(|e| matches!(*e.input_type(), InputType::File(_)));
1310
1311    // Step 7. If file is false or the session's strict file interactability
1312    if !is_file_input || strict_file_interactability {
1313        // Step 7.1. Scroll into view the element
1314        scroll_into_view(cx, &element);
1315
1316        // TODO: Step 7.2 - 7.5
1317        // Wait until element become keyboard-interactable
1318
1319        // Step 7.6. If element is not keyboard-interactable,
1320        // return ErrorStatus::ElementNotInteractable.
1321        if !element.is_keyboard_interactable(cx.no_gc()) {
1322            let _ = reply.send(Err(ErrorStatus::ElementNotInteractable));
1323            return;
1324        }
1325
1326        // Step 7.7. If element is not the active element
1327        // run the focusing steps for the element.
1328        let Some(html_element) = element.downcast::<HTMLElement>() else {
1329            let _ = reply.send(Err(ErrorStatus::UnknownError));
1330            return;
1331        };
1332
1333        if !element.is_active_element() {
1334            html_element.Focus(
1335                cx,
1336                &FocusOptions {
1337                    preventScroll: true,
1338                },
1339            );
1340        } else {
1341            element_has_focus = element.focus_state();
1342        }
1343    }
1344
1345    if let Some(input_element) = input_element {
1346        // Step 8 (Handle file upload)
1347        if is_file_input {
1348            handle_send_keys_file(cx, input_element, &text, reply);
1349            return;
1350        }
1351
1352        // Step 8 (Handle non-typeable form control)
1353        if input_element.is_nontypeable() {
1354            let _ = reply.send(handle_send_keys_non_typeable(cx, input_element, &text));
1355            return;
1356        }
1357    }
1358
1359    // TODO: Check content editable
1360
1361    // Step 8 (Other type of elements)
1362    // Step 8.1. If element does not currently have focus,
1363    // let current text length be the length of element's API value.
1364    // Step 8.2. Set the text insertion caret using set selection range
1365    // using current text length for both the start and end parameters.
1366    if !element_has_focus {
1367        if let Some(input_element) = input_element {
1368            let length = input_element.Value().len_utf16().0;
1369            let _ = input_element.SetSelectionRange(length, length, None);
1370        } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1371            let length = textarea_element.Value().len_utf16().0;
1372            let _ = textarea_element.SetSelectionRange(length, length, None);
1373        }
1374    }
1375
1376    let _ = reply.send(Ok(true));
1377}
1378
1379pub(crate) fn handle_get_active_element(
1380    documents: &DocumentCollection,
1381    pipeline: PipelineId,
1382    reply: GenericSender<Option<String>>,
1383) {
1384    reply
1385        .send(
1386            documents
1387                .find_document(pipeline)
1388                .and_then(|document| document.GetActiveElement())
1389                .map(|element| element.upcast::<Node>().unique_id(pipeline)),
1390        )
1391        .unwrap();
1392}
1393
1394pub(crate) fn handle_get_computed_role(
1395    cx: &mut JSContext,
1396    documents: &DocumentCollection,
1397    pipeline: PipelineId,
1398    node_id: String,
1399    reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1400) {
1401    if !pref!(accessibility_enabled) {
1402        return reply.send(Err(ErrorStatus::UnsupportedOperation)).unwrap();
1403    }
1404    reply
1405        .send(
1406            get_known_element(documents, pipeline, node_id).map(|element| {
1407                let document = element.upcast::<Node>().owner_doc();
1408                let window = document.window();
1409                window
1410                    .layout()
1411                    .set_accessibility_active(true, Epoch::default());
1412                element.get_computed_role(cx).map(String::from)
1413            }),
1414        )
1415        .unwrap();
1416}
1417
1418pub(crate) fn handle_get_page_source(
1419    cx: &mut JSContext,
1420    documents: &DocumentCollection,
1421    pipeline: PipelineId,
1422    reply: GenericSender<Result<String, ErrorStatus>>,
1423) {
1424    reply
1425        .send(
1426            documents
1427                .find_document(pipeline)
1428                .ok_or(ErrorStatus::UnknownError)
1429                .and_then(|document| match document.GetDocumentElement() {
1430                    Some(element) => match element.outer_html(cx) {
1431                        Ok(source) => Ok(String::from(source)),
1432                        Err(_) => {
1433                            match XMLSerializer::new(cx, document.window(), None)
1434                                .SerializeToString(element.upcast::<Node>())
1435                            {
1436                                Ok(source) => Ok(String::from(source)),
1437                                Err(_) => Err(ErrorStatus::UnknownError),
1438                            }
1439                        },
1440                    },
1441                    None => Err(ErrorStatus::UnknownError),
1442                }),
1443        )
1444        .unwrap();
1445}
1446
1447pub(crate) fn handle_get_cookies(
1448    documents: &DocumentCollection,
1449    pipeline: PipelineId,
1450    reply: GenericSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
1451) {
1452    reply
1453        .send(
1454            // TODO: Return an error if the pipeline doesn't exist
1455            match documents.find_document(pipeline) {
1456                Some(document) => {
1457                    let url = document.url();
1458                    let (sender, receiver) = generic_channel::channel().unwrap();
1459                    let _ = document
1460                        .window()
1461                        .as_global_scope()
1462                        .resource_threads()
1463                        .send(GetCookiesForUrl(url, sender, NonHTTP));
1464                    Ok(receiver.recv().unwrap())
1465                },
1466                None => Ok(Vec::new()),
1467            },
1468        )
1469        .unwrap();
1470}
1471
1472/// <https://w3c.github.io/webdriver/webdriver-spec.html#get-cookie>
1473pub(crate) fn handle_get_cookie(
1474    documents: &DocumentCollection,
1475    pipeline: PipelineId,
1476    name: String,
1477    reply: GenericSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
1478) {
1479    reply
1480        .send(
1481            // TODO: Return an error if the pipeline doesn't exist
1482            match documents.find_document(pipeline) {
1483                Some(document) => {
1484                    let url = document.url();
1485                    let (sender, receiver) = generic_channel::channel().unwrap();
1486                    let _ = document
1487                        .window()
1488                        .as_global_scope()
1489                        .resource_threads()
1490                        .send(GetCookiesForUrl(url, sender, NonHTTP));
1491                    let cookies = receiver.recv().unwrap();
1492                    Ok(cookies
1493                        .into_iter()
1494                        .filter(|cookie| cookie.name() == &*name)
1495                        .collect())
1496                },
1497                None => Ok(Vec::new()),
1498            },
1499        )
1500        .unwrap();
1501}
1502
1503/// <https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie>
1504pub(crate) fn handle_add_cookie(
1505    documents: &DocumentCollection,
1506    pipeline: PipelineId,
1507    cookie: Cookie<'static>,
1508    reply: GenericSender<Result<(), ErrorStatus>>,
1509) {
1510    // TODO: Return a different error if the pipeline doesn't exist
1511    let document = match documents.find_document(pipeline) {
1512        Some(document) => document,
1513        None => {
1514            return reply.send(Err(ErrorStatus::NoSuchWindow)).unwrap();
1515        },
1516    };
1517    let url = document.url();
1518    let method = if cookie.http_only().unwrap_or(false) {
1519        HTTP
1520    } else {
1521        NonHTTP
1522    };
1523
1524    let domain = cookie.domain().map(ToOwned::to_owned);
1525    // Step 6.
1526    reply
1527        .send(match (document.is_cookie_averse(), domain) {
1528            // If session's current browsing context's document element is a
1529            // cookie-averse Document object, return error with error code invalid cookie domain.
1530            (true, _) => Err(ErrorStatus::InvalidCookieDomain),
1531            (false, Some(ref domain)) if url.host_str().is_some_and(|host| host == domain) => {
1532                let _ = document
1533                    .window()
1534                    .as_global_scope()
1535                    .resource_threads()
1536                    .send(SetCookieForUrl(url, Serde(cookie), method, None));
1537                Ok(())
1538            },
1539            // If cookie domain is not equal to session's current browsing context's
1540            // active document's domain, return error with error code invalid cookie domain.
1541            (false, Some(_)) => Err(ErrorStatus::InvalidCookieDomain),
1542            (false, None) => {
1543                let _ = document
1544                    .window()
1545                    .as_global_scope()
1546                    .resource_threads()
1547                    .send(SetCookieForUrl(url, Serde(cookie), method, None));
1548                Ok(())
1549            },
1550        })
1551        .unwrap();
1552}
1553
1554/// <https://w3c.github.io/webdriver/#delete-all-cookies>
1555pub(crate) fn handle_delete_cookies(
1556    documents: &DocumentCollection,
1557    pipeline: PipelineId,
1558    reply: GenericSender<Result<(), ErrorStatus>>,
1559) {
1560    let document = match documents.find_document(pipeline) {
1561        Some(document) => document,
1562        None => {
1563            return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
1564        },
1565    };
1566    let url = document.url();
1567    document
1568        .window()
1569        .as_global_scope()
1570        .resource_threads()
1571        .send(DeleteCookies(Some(url), None))
1572        .unwrap();
1573    reply.send(Ok(())).unwrap();
1574}
1575
1576/// <https://w3c.github.io/webdriver/#delete-cookie>
1577pub(crate) fn handle_delete_cookie(
1578    documents: &DocumentCollection,
1579    pipeline: PipelineId,
1580    name: String,
1581    reply: GenericSender<Result<(), ErrorStatus>>,
1582) {
1583    let document = match documents.find_document(pipeline) {
1584        Some(document) => document,
1585        None => {
1586            return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
1587        },
1588    };
1589    let url = document.url();
1590    document
1591        .window()
1592        .as_global_scope()
1593        .resource_threads()
1594        .send(DeleteCookie(url, name))
1595        .unwrap();
1596    reply.send(Ok(())).unwrap();
1597}
1598
1599pub(crate) fn handle_get_title(
1600    documents: &DocumentCollection,
1601    pipeline: PipelineId,
1602    reply: GenericSender<String>,
1603) {
1604    reply
1605        .send(
1606            // TODO: Return an error if the pipeline doesn't exist
1607            documents
1608                .find_document(pipeline)
1609                .map(|document| String::from(document.Title()))
1610                .unwrap_or_default(),
1611        )
1612        .unwrap();
1613}
1614
1615/// <https://w3c.github.io/webdriver/#dfn-calculate-the-absolute-position>
1616fn calculate_absolute_position(
1617    documents: &DocumentCollection,
1618    pipeline: &PipelineId,
1619    rect: &DOMRect,
1620) -> Result<(f64, f64), ErrorStatus> {
1621    // Step 1
1622    // We already pass the rectangle here, see `handle_get_rect`.
1623
1624    // Step 2
1625    let document = match documents.find_document(*pipeline) {
1626        Some(document) => document,
1627        None => return Err(ErrorStatus::UnknownError),
1628    };
1629    let win = match document.GetDefaultView() {
1630        Some(win) => win,
1631        None => return Err(ErrorStatus::UnknownError),
1632    };
1633
1634    // Step 3 - 5
1635    let x = win.ScrollX() as f64 + rect.X();
1636    let y = win.ScrollY() as f64 + rect.Y();
1637
1638    Ok((x, y))
1639}
1640
1641/// <https://w3c.github.io/webdriver/#get-element-rect>
1642pub(crate) fn handle_get_rect(
1643    cx: &mut JSContext,
1644    documents: &DocumentCollection,
1645    pipeline: PipelineId,
1646    element_id: String,
1647    reply: GenericSender<Result<Rect<f64>, ErrorStatus>>,
1648) {
1649    reply
1650        .send(
1651            get_known_element(documents, pipeline, element_id).and_then(|element| {
1652                // Step 4-5
1653                // We pass the rect instead of element so we don't have to
1654                // call `GetBoundingClientRect` twice.
1655                let rect = element.GetBoundingClientRect(cx);
1656                let (x, y) = calculate_absolute_position(documents, &pipeline, &rect)?;
1657
1658                // Step 6-7
1659                Ok(Rect::new(
1660                    Point2D::new(x, y),
1661                    Size2D::new(rect.Width(), rect.Height()),
1662                ))
1663            }),
1664        )
1665        .unwrap();
1666}
1667
1668pub(crate) fn handle_scroll_and_get_bounding_client_rect(
1669    cx: &mut JSContext,
1670    documents: &DocumentCollection,
1671    pipeline: PipelineId,
1672    element_id: String,
1673    reply: GenericSender<Result<Rect<f32>, ErrorStatus>>,
1674) {
1675    reply
1676        .send(
1677            get_known_element(documents, pipeline, element_id).map(|element| {
1678                scroll_into_view(cx, &element);
1679
1680                let rect = element.GetBoundingClientRect(cx);
1681                Rect::new(
1682                    Point2D::new(rect.X() as f32, rect.Y() as f32),
1683                    Size2D::new(rect.Width() as f32, rect.Height() as f32),
1684                )
1685            }),
1686        )
1687        .unwrap();
1688}
1689
1690/// <https://w3c.github.io/webdriver/#dfn-get-element-text>
1691pub(crate) fn handle_get_text(
1692    documents: &DocumentCollection,
1693    pipeline: PipelineId,
1694    node_id: String,
1695    reply: GenericSender<Result<String, ErrorStatus>>,
1696) {
1697    reply
1698        .send(
1699            get_known_element(documents, pipeline, node_id).map(|element| {
1700                element
1701                    .downcast::<HTMLElement>()
1702                    .map(|htmlelement| String::from(htmlelement.InnerText()))
1703                    .unwrap_or_else(|| {
1704                        element
1705                            .upcast::<Node>()
1706                            .GetTextContent()
1707                            .map_or(String::new(), String::from)
1708                    })
1709            }),
1710        )
1711        .unwrap();
1712}
1713
1714/// <https://w3c.github.io/webdriver/#dfn-get-element-tag-name>
1715pub(crate) fn handle_get_name(
1716    documents: &DocumentCollection,
1717    pipeline: PipelineId,
1718    node_id: String,
1719    reply: GenericSender<Result<String, ErrorStatus>>,
1720) {
1721    reply
1722        .send(
1723            get_known_element(documents, pipeline, node_id)
1724                .map(|element| element.qualified_name().into_owned()),
1725        )
1726        .unwrap();
1727}
1728
1729pub(crate) fn handle_get_attribute(
1730    cx: &mut JSContext,
1731    documents: &DocumentCollection,
1732    pipeline: PipelineId,
1733    node_id: String,
1734    name: String,
1735    reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1736) {
1737    reply
1738        .send(
1739            get_known_element(documents, pipeline, node_id).map(|element| {
1740                if is_boolean_attribute(&name) {
1741                    if element.HasAttribute(cx, DOMString::from(name)) {
1742                        Some(String::from("true"))
1743                    } else {
1744                        None
1745                    }
1746                } else {
1747                    element
1748                        .GetAttribute(cx, DOMString::from(name))
1749                        .map(String::from)
1750                }
1751            }),
1752        )
1753        .unwrap();
1754}
1755
1756pub(crate) fn handle_get_property(
1757    cx: &mut JSContext,
1758    documents: &DocumentCollection,
1759    pipeline: PipelineId,
1760    node_id: String,
1761    name: String,
1762    reply: GenericSender<Result<JSValue, ErrorStatus>>,
1763) {
1764    reply
1765        .send(
1766            get_known_element(documents, pipeline, node_id).map(|element| {
1767                let document = documents.find_document(pipeline).unwrap();
1768
1769                let Ok(cname) = CString::new(name) else {
1770                    return JSValue::Undefined;
1771                };
1772
1773                let mut realm = enter_auto_realm(cx, &*document);
1774                let cx = &mut realm.current_realm();
1775
1776                rooted!(&in(cx) let mut property = UndefinedValue());
1777                match get_property_jsval(
1778                    cx,
1779                    element.reflector().get_jsobject(),
1780                    &cname,
1781                    property.handle_mut(),
1782                ) {
1783                    Ok(_) => match jsval_to_webdriver(cx, &element.global(), property.handle()) {
1784                        Ok(property) => property,
1785                        Err(_) => JSValue::Undefined,
1786                    },
1787                    Err(error) => {
1788                        throw_dom_exception(cx, &element.global(), error);
1789                        JSValue::Undefined
1790                    },
1791                }
1792            }),
1793        )
1794        .unwrap();
1795}
1796
1797pub(crate) fn handle_get_css(
1798    cx: &mut JSContext,
1799    documents: &DocumentCollection,
1800    pipeline: PipelineId,
1801    node_id: String,
1802    name: String,
1803    reply: GenericSender<Result<String, ErrorStatus>>,
1804) {
1805    reply
1806        .send(
1807            get_known_element(documents, pipeline, node_id).map(|element| {
1808                let window = element.owner_window();
1809                String::from(
1810                    window
1811                        .GetComputedStyle(cx, &element, None)
1812                        .GetPropertyValue(DOMString::from(name)),
1813                )
1814            }),
1815        )
1816        .unwrap();
1817}
1818
1819pub(crate) fn handle_get_url(
1820    documents: &DocumentCollection,
1821    pipeline: PipelineId,
1822    reply: GenericSender<String>,
1823) {
1824    reply
1825        .send(
1826            // TODO: Return an error if the pipeline doesn't exist.
1827            documents
1828                .find_document(pipeline)
1829                .map(|document| document.url().into_string())
1830                .unwrap_or_else(|| "about:blank".to_string()),
1831        )
1832        .unwrap();
1833}
1834
1835/// <https://w3c.github.io/webdriver/#dfn-mutable-form-control-element>
1836fn element_is_mutable_form_control(element: &Element) -> bool {
1837    if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1838        input_element.is_mutable() &&
1839            matches!(
1840                *input_element.input_type(),
1841                InputType::Text(_) |
1842                    InputType::Search(_) |
1843                    InputType::Url(_) |
1844                    InputType::Tel(_) |
1845                    InputType::Email(_) |
1846                    InputType::Password(_) |
1847                    InputType::Date(_) |
1848                    InputType::Month(_) |
1849                    InputType::Week(_) |
1850                    InputType::Time(_) |
1851                    InputType::DatetimeLocal(_) |
1852                    InputType::Number(_) |
1853                    InputType::Range(_) |
1854                    InputType::Color(_) |
1855                    InputType::File(_)
1856            )
1857    } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1858        textarea_element.is_mutable()
1859    } else {
1860        false
1861    }
1862}
1863
1864/// <https://w3c.github.io/webdriver/#dfn-clear-a-resettable-element>
1865fn clear_a_resettable_element(cx: &mut JSContext, element: &Element) -> Result<(), ErrorStatus> {
1866    let html_element = element
1867        .downcast::<HTMLElement>()
1868        .ok_or(ErrorStatus::UnknownError)?;
1869
1870    // Step 1 - 2. if element is a candidate for constraint
1871    // validation and value is empty, abort steps.
1872    if html_element.is_candidate_for_constraint_validation(cx.no_gc()) {
1873        if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1874            if input_element.Value().is_empty() {
1875                return Ok(());
1876            }
1877        } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() &&
1878            textarea_element.Value().is_empty()
1879        {
1880            return Ok(());
1881        }
1882    }
1883
1884    // Step 3. Invoke the focusing steps for the element.
1885    html_element.Focus(
1886        cx,
1887        &FocusOptions {
1888            preventScroll: true,
1889        },
1890    );
1891
1892    // Step 4. Run clear algorithm for element.
1893    if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1894        input_element.clear(cx);
1895    } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1896        textarea_element.clear();
1897    } else {
1898        unreachable!("We have confirm previously that element is mutable form control");
1899    }
1900
1901    let event_target = element.upcast::<EventTarget>();
1902    event_target.fire_bubbling_event(cx, atom!("input"));
1903    event_target.fire_bubbling_event(cx, atom!("change"));
1904
1905    // Step 5. Run the unfocusing steps for the element.
1906    html_element.Blur(cx);
1907
1908    Ok(())
1909}
1910
1911/// <https://w3c.github.io/webdriver/#element-clear>
1912pub(crate) fn handle_element_clear(
1913    cx: &mut JSContext,
1914    documents: &DocumentCollection,
1915    pipeline: PipelineId,
1916    element_id: String,
1917    reply: GenericSender<Result<(), ErrorStatus>>,
1918) {
1919    reply
1920        .send(
1921            get_known_element(documents, pipeline, element_id).and_then(|element| {
1922                // Step 4. If element is not editable, return ErrorStatus::InvalidElementState.
1923                // TODO: editing hosts and content editable elements are not implemented yet,
1924                // hence we currently skip the check
1925                if !element_is_mutable_form_control(&element) {
1926                    return Err(ErrorStatus::InvalidElementState);
1927                }
1928
1929                // Step 5. Scroll Into View
1930                scroll_into_view(cx, &element);
1931
1932                // TODO: Step 6 - 9: Implicit wait. In another PR.
1933                // Wait until element become interactable and check.
1934
1935                // Step 10. If element is not keyboard-interactable or not pointer-interactable,
1936                // return error with error code element not interactable.
1937                if !element.is_keyboard_interactable(cx.no_gc()) {
1938                    return Err(ErrorStatus::ElementNotInteractable);
1939                }
1940
1941                let paint_tree = get_element_pointer_interactable_paint_tree(cx, &element);
1942                if !is_element_in_view(&element, &paint_tree) {
1943                    return Err(ErrorStatus::ElementNotInteractable);
1944                }
1945
1946                // Step 11
1947                // TODO: Clear content editable elements
1948                clear_a_resettable_element(cx, &element)
1949            }),
1950        )
1951        .unwrap();
1952}
1953
1954fn get_option_parent(node: &Node) -> Option<DomRoot<Element>> {
1955    // Get parent for `<option>` or `<optiongrp>` based on container spec:
1956    // > 1. Let datalist parent be the first datalist element reached by traversing the tree
1957    // >    in reverse order from element, or undefined if the root of the tree is reached.
1958    // > 2. Let select parent be the first select element reached by traversing the tree in
1959    // >    reverse order from element, or undefined if the root of the tree is reached.
1960    // > 3. If datalist parent is undefined, the element context is select parent.
1961    // >    Otherwise, the element context is datalist parent.
1962    let mut candidate_select = None;
1963
1964    for ancestor in node.ancestors() {
1965        if ancestor.is::<HTMLDataListElement>() {
1966            return Some(DomRoot::downcast::<Element>(ancestor).unwrap());
1967        } else if candidate_select.is_none() && ancestor.is::<HTMLSelectElement>() {
1968            candidate_select = Some(ancestor);
1969        }
1970    }
1971
1972    candidate_select.map(|ancestor| DomRoot::downcast::<Element>(ancestor).unwrap())
1973}
1974
1975/// <https://w3c.github.io/webdriver/#dfn-container>
1976fn get_container(element: &Element) -> Option<DomRoot<Element>> {
1977    if element.is::<HTMLOptionElement>() {
1978        return get_option_parent(element.upcast::<Node>());
1979    }
1980    if element.is::<HTMLOptGroupElement>() {
1981        return get_option_parent(element.upcast::<Node>())
1982            .or_else(|| Some(DomRoot::from_ref(element)));
1983    }
1984    Some(DomRoot::from_ref(element))
1985}
1986
1987/// <https://w3c.github.io/webdriver/#element-click>
1988pub(crate) fn handle_element_click(
1989    cx: &mut JSContext,
1990    documents: &DocumentCollection,
1991    pipeline: PipelineId,
1992    element_id: String,
1993    reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1994) {
1995    reply
1996        .send(
1997            // Step 3
1998            get_known_element(documents, pipeline, element_id).and_then(|element| {
1999                // Step 4. If the element is an input element in the file upload state
2000                // return error with error code invalid argument.
2001                if let Some(input_element) = element.downcast::<HTMLInputElement>() &&
2002                    matches!(*input_element.input_type(), InputType::File(_))
2003                {
2004                    return Err(ErrorStatus::InvalidArgument);
2005                }
2006
2007                let Some(container) = get_container(&element) else {
2008                    return Err(ErrorStatus::UnknownError);
2009                };
2010
2011                // Step 5. Scroll into view the element's container.
2012                scroll_into_view(cx, &container);
2013
2014                // Step 6. If element's container is still not in view
2015                // return error with error code element not interactable.
2016                let paint_tree = get_element_pointer_interactable_paint_tree(cx, &container);
2017
2018                if !is_element_in_view(&container, &paint_tree) {
2019                    return Err(ErrorStatus::ElementNotInteractable);
2020                }
2021
2022                // Step 7. If element's container is obscured by another element,
2023                // return error with error code element click intercepted.
2024                // https://w3c.github.io/webdriver/#dfn-obscuring
2025                // An element is obscured if the pointer-interactable paint tree is empty,
2026                // or the first element in this tree is not an inclusive descendant of itself.
2027                // `paint_tree` is guaranteed not empty as element is "in view".
2028                if !container
2029                    .upcast::<Node>()
2030                    .is_shadow_including_inclusive_ancestor_of(paint_tree[0].upcast::<Node>())
2031                {
2032                    return Err(ErrorStatus::ElementClickIntercepted);
2033                }
2034
2035                // Step 8 for <option> element.
2036                match element.downcast::<HTMLOptionElement>() {
2037                    Some(option_element) => {
2038                        // Steps 8.2 - 8.4
2039                        let event_target = container.upcast::<EventTarget>();
2040                        event_target.fire_event(cx, atom!("mouseover"));
2041                        event_target.fire_event(cx, atom!("mousemove"));
2042                        event_target.fire_event(cx, atom!("mousedown"));
2043
2044                        // Step 8.5
2045                        match container.downcast::<HTMLElement>() {
2046                            Some(html_element) => {
2047                                html_element.Focus(
2048                                    cx,
2049                                    &FocusOptions {
2050                                        preventScroll: true,
2051                                    },
2052                                );
2053                            },
2054                            None => return Err(ErrorStatus::UnknownError),
2055                        }
2056
2057                        // Step 8.6
2058                        if !is_disabled(&element) {
2059                            // Step 8.6.1
2060                            event_target.fire_event(cx, atom!("input"));
2061
2062                            // Steps 8.6.2
2063                            let previous_selectedness = option_element.Selected();
2064
2065                            // Step 8.6.3
2066                            match container.downcast::<HTMLSelectElement>() {
2067                                Some(select_element) => {
2068                                    if select_element.Multiple() {
2069                                        option_element.SetSelected(cx, !option_element.Selected());
2070                                    }
2071                                },
2072                                None => option_element.SetSelected(cx, true),
2073                            }
2074
2075                            // Step 8.6.4
2076                            if !previous_selectedness {
2077                                event_target.fire_event(cx, atom!("change"));
2078                            }
2079                        }
2080
2081                        // Steps 8.7 - 8.8
2082                        event_target.fire_event(cx, atom!("mouseup"));
2083                        event_target.fire_event(cx, atom!("click"));
2084
2085                        Ok(None)
2086                    },
2087                    None => Ok(Some(element.upcast::<Node>().unique_id(pipeline))),
2088                }
2089            }),
2090        )
2091        .unwrap();
2092}
2093
2094/// <https://w3c.github.io/webdriver/#dfn-in-view>
2095fn is_element_in_view(element: &Element, paint_tree: &[DomRoot<Element>]) -> bool {
2096    // An element is in view if it is a member of its own pointer-interactable paint tree,
2097    // given the pretense that its pointer events are not disabled.
2098    if !paint_tree.iter().any(|e| &**e == element) {
2099        return false;
2100    }
2101    use style::computed_values::pointer_events::T as PointerEvents;
2102    // https://w3c.github.io/webdriver/#dfn-pointer-events-are-not-disabled
2103    // An element is said to have pointer events disabled
2104    // if the resolved value of its "pointer-events" style property is "none".
2105    element
2106        .style()
2107        .is_none_or(|style| style.get_inherited_ui().pointer_events != PointerEvents::None)
2108}
2109
2110/// <https://w3c.github.io/webdriver/#dfn-pointer-interactable-paint-tree>
2111fn get_element_pointer_interactable_paint_tree(
2112    cx: &mut JSContext,
2113    element: &Element,
2114) -> Vec<DomRoot<Element>> {
2115    // Step 1. If element is not in the same tree as session's
2116    // current browsing context's active document, return an empty sequence.
2117    if !element.is_connected() {
2118        return Vec::new();
2119    }
2120
2121    // Step 2 - 5: Return "elements from point" w.r.t. in-view center point of element.
2122    // Spec has bugs in description and can be simplified.
2123    // The original step 4 "compute in-view center point" takes an element as argument
2124    // which internally computes first DOMRect of getClientRects
2125
2126    get_element_in_view_center_point(cx, element).map_or(Vec::new(), |center_point| {
2127        if let Some(shadow_root) = element.containing_shadow_root() {
2128            shadow_root.ElementsFromPoint(
2129                Finite::wrap(center_point.x as f64),
2130                Finite::wrap(center_point.y as f64),
2131            )
2132        } else {
2133            element.owner_document().ElementsFromPoint(
2134                Finite::wrap(center_point.x as f64),
2135                Finite::wrap(center_point.y as f64),
2136            )
2137        }
2138    })
2139}
2140
2141/// <https://w3c.github.io/webdriver/#is-element-enabled>
2142pub(crate) fn handle_is_enabled(
2143    documents: &DocumentCollection,
2144    pipeline: PipelineId,
2145    element_id: String,
2146    reply: GenericSender<Result<bool, ErrorStatus>>,
2147) {
2148    reply
2149        .send(
2150            // Step 3. Let element be the result of trying to get a known element
2151            get_known_element(documents, pipeline, element_id).map(|element| {
2152                // In `get_known_element`, we confirmed that document exists
2153                let document = documents.find_document(pipeline).unwrap();
2154
2155                // Step 4
2156                // Let enabled be a boolean initially set to true if session's
2157                // current browsing context's active document's type is not "xml".
2158                // Otherwise, let enabled to false and jump to the last step of this algorithm.
2159                // Step 5. Set enabled to false if a form control is disabled.
2160                if document.is_html_document() || document.is_xhtml_document() {
2161                    !is_disabled(&element)
2162                } else {
2163                    false
2164                }
2165            }),
2166        )
2167        .unwrap();
2168}
2169
2170pub(crate) fn handle_is_selected(
2171    documents: &DocumentCollection,
2172    pipeline: PipelineId,
2173    element_id: String,
2174    reply: GenericSender<Result<bool, ErrorStatus>>,
2175) {
2176    reply
2177        .send(
2178            get_known_element(documents, pipeline, element_id).and_then(|element| {
2179                if let Some(input_element) = element.downcast::<HTMLInputElement>() {
2180                    Ok(input_element.Checked())
2181                } else if let Some(option_element) = element.downcast::<HTMLOptionElement>() {
2182                    Ok(option_element.Selected())
2183                } else if element.is::<HTMLElement>() {
2184                    Ok(false) // regular elements are not selectable
2185                } else {
2186                    Err(ErrorStatus::UnknownError)
2187                }
2188            }),
2189        )
2190        .unwrap();
2191}
2192
2193pub(crate) fn handle_add_load_status_sender(
2194    documents: &DocumentCollection,
2195    pipeline: PipelineId,
2196    reply: GenericSender<WebDriverLoadStatus>,
2197) {
2198    if let Some(document) = documents.find_document(pipeline) {
2199        let window = document.window();
2200        window.set_webdriver_load_status_sender(Some(reply));
2201    }
2202}
2203
2204pub(crate) fn handle_remove_load_status_sender(
2205    documents: &DocumentCollection,
2206    pipeline: PipelineId,
2207) {
2208    if let Some(document) = documents.find_document(pipeline) {
2209        let window = document.window();
2210        window.set_webdriver_load_status_sender(None);
2211    }
2212}
2213
2214/// <https://w3c.github.io/webdriver/#dfn-scrolls-into-view>
2215fn scroll_into_view(cx: &mut JSContext, element: &Element) {
2216    // Check if element is already in view
2217    let paint_tree = get_element_pointer_interactable_paint_tree(cx, element);
2218    if is_element_in_view(element, &paint_tree) {
2219        return;
2220    }
2221
2222    // Step 1. Let options be the following ScrollIntoViewOptions:
2223    // - "behavior": instant
2224    // - Logical scroll position "block": end
2225    // - Logical scroll position "inline": nearest
2226    let options = BooleanOrScrollIntoViewOptions::ScrollIntoViewOptions(ScrollIntoViewOptions {
2227        parent: ScrollOptions {
2228            behavior: ScrollBehavior::Instant,
2229        },
2230        block: ScrollLogicalPosition::End,
2231        inline: ScrollLogicalPosition::Nearest,
2232        container: Default::default(),
2233    });
2234    // Step 2. Run scrollIntoView
2235    element.ScrollIntoView(cx, options);
2236}
2237
2238pub(crate) fn set_protocol_handler_automation_mode(
2239    documents: &DocumentCollection,
2240    pipeline: PipelineId,
2241    mode: CustomHandlersAutomationMode,
2242) {
2243    if let Some(document) = documents.find_document(pipeline) {
2244        document.set_protocol_handler_automation_mode(mode);
2245    }
2246}
2247
2248/// <https://www.w3.org/TR/permissions/#webdriver-command-set-permission>
2249pub(crate) fn set_permission(
2250    documents: &DocumentCollection,
2251    pipeline: PipelineId,
2252    name: String,
2253    state: SetPermissionState,
2254    reply: GenericOneshotSender<Result<(), ErrorStatus>>,
2255) {
2256    let Ok(name) = name.parse::<PermissionName>() else {
2257        if let Err(err) = reply.send(Err(ErrorStatus::InvalidArgument)) {
2258            error!("SetPermission Failed to send reply: {err}");
2259        }
2260        return;
2261    };
2262    let state = match state {
2263        SetPermissionState::Denied => PermissionState::Denied,
2264        SetPermissionState::Granted => PermissionState::Granted,
2265        SetPermissionState::Prompt => PermissionState::Prompt,
2266    };
2267
2268    let Some(global) = documents.find_global(pipeline) else {
2269        if let Err(err) = reply.send(Err(ErrorStatus::NoSuchWindow)) {
2270            error!("SetPermission Failed to send reply: {err}");
2271        }
2272        return;
2273    };
2274
2275    // TODO: Make this per-origin instead of per-document/globalscope according to spec.
2276    global
2277        .permission_state_invocation_results()
2278        .borrow_mut()
2279        .insert(name, state);
2280
2281    // TODO: dispatch "change" event.
2282    // This is currently impossible because eventtarget is not registered.
2283
2284    if let Err(err) = reply.send(Ok(())) {
2285        error!("SetPermission Failed to send reply: {err}");
2286    }
2287}