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