Skip to main content

script/event_loop/
webdriver_handlers.rs

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