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