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