Skip to main content

script/event_loop/
webdriver_handlers.rs

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