script/
webdriver_handlers.rs

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