Skip to main content

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