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