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