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