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