1use std::collections::{HashMap, HashSet};
6use std::ffi::CString;
7use std::ptr::NonNull;
8
9use cookie::Cookie;
10use embedder_traits::{
11 CustomHandlersAutomationMode, JSValue, JavaScriptEvaluationError,
12 JavaScriptEvaluationResultSerializationError, WebDriverFrameId, WebDriverJSResult,
13 WebDriverLoadStatus,
14};
15use euclid::default::{Point2D, Rect, Size2D};
16use hyper_serde::Serde;
17use js::context::JSContext;
18use js::conversions::{FromJSValConvertible, jsstr_to_string};
19use js::jsapi::{HandleValueArray, JSITER_OWNONLY, JSType, PropertyDescriptor};
20use js::jsval::UndefinedValue;
21use js::realm::CurrentRealm;
22use js::rust::wrappers2::{
23 GetPropertyKeys, JS_CallFunctionName, JS_GetOwnPropertyDescriptorById, JS_GetProperty,
24 JS_GetPropertyById, JS_HasOwnProperty, JS_IsExceptionPending, JS_TypeOfValue,
25};
26use js::rust::{HandleObject, HandleValue, IdVector, ToString};
27use net_traits::CookieSource::{HTTP, NonHTTP};
28use net_traits::CoreResourceMsg::{DeleteCookie, DeleteCookies, GetCookiesForUrl, SetCookieForUrl};
29use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
30use script_bindings::conversions::is_array_like;
31use script_bindings::num::Finite;
32use script_bindings::reflector::DomObject;
33use script_bindings::settings_stack::run_a_script;
34use servo_base::generic_channel::{self, GenericOneshotSender, GenericSend, GenericSender};
35use servo_base::id::{BrowsingContextId, PipelineId};
36use webdriver::error::ErrorStatus;
37
38use crate::DomTypeHolder;
39use crate::document_collection::DocumentCollection;
40use crate::dom::attr::is_boolean_attribute;
41use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
42use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
43use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
44use crate::dom::bindings::codegen::Bindings::ElementBinding::{
45 ElementMethods, ScrollIntoViewOptions, ScrollLogicalPosition,
46};
47use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
48use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
49use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
50use crate::dom::bindings::codegen::Bindings::HTMLOrSVGElementBinding::FocusOptions;
51use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods;
52use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
53use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
54use crate::dom::bindings::codegen::Bindings::WindowBinding::{
55 ScrollBehavior, ScrollOptions, WindowMethods,
56};
57use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializerMethods;
58use crate::dom::bindings::codegen::Bindings::XPathResultBinding::{
59 XPathResultConstants, XPathResultMethods,
60};
61use crate::dom::bindings::codegen::UnionTypes::BooleanOrScrollIntoViewOptions;
62use crate::dom::bindings::conversions::{
63 ConversionBehavior, ConversionResult, get_property, get_property_jsval, jsid_to_string,
64 root_from_object,
65};
66use crate::dom::bindings::error::{Error, report_pending_exception, throw_dom_exception};
67use crate::dom::bindings::inheritance::Castable;
68use crate::dom::bindings::reflector::DomGlobal;
69use crate::dom::bindings::root::DomRoot;
70use crate::dom::bindings::str::DOMString;
71use crate::dom::document::Document;
72use crate::dom::domrect::DOMRect;
73use crate::dom::element::Element;
74use crate::dom::eventtarget::EventTarget;
75use crate::dom::globalscope::GlobalScope;
76use crate::dom::html::htmlbodyelement::HTMLBodyElement;
77use crate::dom::html::htmldatalistelement::HTMLDataListElement;
78use crate::dom::html::htmlelement::HTMLElement;
79use crate::dom::html::htmlformelement::FormControl;
80use crate::dom::html::htmliframeelement::HTMLIFrameElement;
81use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement;
82use crate::dom::html::htmloptionelement::HTMLOptionElement;
83use crate::dom::html::htmlselectelement::HTMLSelectElement;
84use crate::dom::html::htmltextareaelement::HTMLTextAreaElement;
85use crate::dom::html::input_element::HTMLInputElement;
86use crate::dom::input_element::input_type::InputType;
87use crate::dom::iterators::ShadowIncluding;
88use crate::dom::node::{Node, NodeTraits};
89use crate::dom::nodelist::NodeList;
90use crate::dom::types::ShadowRoot;
91use crate::dom::validitystate::ValidationFlags;
92use crate::dom::window::Window;
93use crate::dom::xmlserializer::XMLSerializer;
94use crate::realms::enter_auto_realm;
95use crate::script_runtime::CanGc;
96use crate::script_thread::ScriptThread;
97
98fn is_stale(element: &Element) -> bool {
100 !element.owner_document().is_active() || !element.is_connected()
103}
104
105fn is_detached(shadow_root: &ShadowRoot) -> bool {
107 !shadow_root.owner_document().is_active() || is_stale(&shadow_root.Host())
110}
111
112fn is_disabled(element: &Element) -> bool {
114 if element.is::<HTMLOptionElement>() || element.is::<HTMLOptGroupElement>() {
116 let disabled = element
118 .upcast::<Node>()
119 .inclusive_ancestors(ShadowIncluding::No)
120 .any(|node| {
121 if node.is::<HTMLOptGroupElement>() || node.is::<HTMLSelectElement>() {
122 node.downcast::<Element>().unwrap().is_actually_disabled()
125 } else {
126 false
127 }
128 });
129
130 if disabled {
135 return true;
136 }
137 }
138 element.is_actually_disabled()
140}
141
142pub(crate) fn handle_get_known_window(
143 documents: &DocumentCollection,
144 pipeline: PipelineId,
145 webview_id: String,
146 reply: GenericSender<Result<(), ErrorStatus>>,
147) {
148 if reply
149 .send(
150 documents
151 .find_window(pipeline)
152 .map_or(Err(ErrorStatus::NoSuchWindow), |window| {
153 let window_proxy = window.window_proxy();
154 if window_proxy.browsing_context_id() != window_proxy.webview_id() ||
156 window_proxy.webview_id().to_string() != webview_id
157 {
158 Err(ErrorStatus::NoSuchWindow)
159 } else {
160 Ok(())
161 }
162 }),
163 )
164 .is_err()
165 {
166 error!("Webdriver get known window reply failed");
167 }
168}
169
170pub(crate) fn handle_get_known_shadow_root(
171 documents: &DocumentCollection,
172 pipeline: PipelineId,
173 shadow_root_id: String,
174 reply: GenericSender<Result<(), ErrorStatus>>,
175) {
176 let result = get_known_shadow_root(documents, pipeline, shadow_root_id).map(|_| ());
177 if reply.send(result).is_err() {
178 error!("Webdriver get known shadow root reply failed");
179 }
180}
181
182fn get_known_shadow_root(
184 documents: &DocumentCollection,
185 pipeline: PipelineId,
186 node_id: String,
187) -> Result<DomRoot<ShadowRoot>, ErrorStatus> {
188 let doc = documents
189 .find_document(pipeline)
190 .ok_or(ErrorStatus::NoSuchWindow)?;
191 if !ScriptThread::has_node_id(pipeline, &node_id) {
194 return Err(ErrorStatus::NoSuchShadowRoot);
195 }
196
197 let node = find_node_by_unique_id_in_document(&doc, node_id);
200
201 if let Some(ref node) = node &&
204 !node.is::<ShadowRoot>()
205 {
206 return Err(ErrorStatus::NoSuchShadowRoot);
207 }
208
209 let Some(node) = node else {
211 return Err(ErrorStatus::DetachedShadowRoot);
212 };
213
214 let shadow_root = DomRoot::downcast::<ShadowRoot>(node).unwrap();
218 if is_detached(&shadow_root) {
219 return Err(ErrorStatus::DetachedShadowRoot);
220 }
221 Ok(shadow_root)
223}
224
225pub(crate) fn handle_get_known_element(
226 documents: &DocumentCollection,
227 pipeline: PipelineId,
228 element_id: String,
229 reply: GenericSender<Result<(), ErrorStatus>>,
230) {
231 let result = get_known_element(documents, pipeline, element_id).map(|_| ());
232 if reply.send(result).is_err() {
233 error!("Webdriver get known element reply failed");
234 }
235}
236
237fn get_known_element(
239 documents: &DocumentCollection,
240 pipeline: PipelineId,
241 node_id: String,
242) -> Result<DomRoot<Element>, ErrorStatus> {
243 let doc = documents
244 .find_document(pipeline)
245 .ok_or(ErrorStatus::NoSuchWindow)?;
246 if !ScriptThread::has_node_id(pipeline, &node_id) {
249 return Err(ErrorStatus::NoSuchElement);
250 }
251 let node = find_node_by_unique_id_in_document(&doc, node_id);
254
255 if let Some(ref node) = node &&
258 !node.is::<Element>()
259 {
260 return Err(ErrorStatus::NoSuchElement);
261 }
262 let Some(node) = node else {
264 return Err(ErrorStatus::StaleElementReference);
265 };
266 let element = DomRoot::downcast::<Element>(node).unwrap();
268 if is_stale(&element) {
269 return Err(ErrorStatus::StaleElementReference);
270 }
271 Ok(element)
273}
274
275pub(crate) fn find_node_by_unique_id_in_document(
277 document: &Document,
278 node_id: String,
279) -> Option<DomRoot<Node>> {
280 let pipeline = document.window().pipeline_id();
281 document
282 .upcast::<Node>()
283 .traverse_preorder(ShadowIncluding::Yes)
284 .find(|node| node.unique_id(pipeline) == node_id)
285}
286
287fn matching_links(
289 links: &NodeList,
290 link_text: String,
291 partial: bool,
292) -> impl Iterator<Item = String> + '_ {
293 links
294 .iter()
295 .filter(move |node| {
296 let content = node
297 .downcast::<HTMLElement>()
298 .map(|element| element.InnerText())
299 .map_or("".to_owned(), String::from)
300 .trim()
301 .to_owned();
302 if partial {
303 content.contains(&link_text)
304 } else {
305 content == link_text
306 }
307 })
308 .map(|node| node.unique_id(node.owner_doc().window().pipeline_id()))
309}
310
311fn all_matching_links(
312 cx: &mut JSContext,
313 root_node: &Node,
314 link_text: String,
315 partial: bool,
316) -> Result<Vec<String>, ErrorStatus> {
317 root_node
321 .query_selector_all(cx.no_gc(), DOMString::from("a"))
322 .map_err(|_| ErrorStatus::InvalidSelector)
323 .map(|nodes| matching_links(&nodes, link_text, partial).collect())
324}
325
326#[expect(unsafe_code)]
327fn object_has_to_json_property(
328 cx: &mut JSContext,
329 global_scope: &GlobalScope,
330 object: HandleObject,
331) -> bool {
332 let name = CString::new("toJSON").unwrap();
333 let mut found = false;
334 if unsafe { JS_HasOwnProperty(cx, object, name.as_ptr(), &mut found) } && found {
335 rooted!(&in(cx) let mut value = UndefinedValue());
336 let result = unsafe { JS_GetProperty(cx, object, name.as_ptr(), value.handle_mut()) };
337 if !result {
338 throw_dom_exception(cx.into(), global_scope, Error::JSFailed, CanGc::from_cx(cx));
339 false
340 } else {
341 result && unsafe { JS_TypeOfValue(cx, value.handle()) } == JSType::JSTYPE_FUNCTION
342 }
343 } else if unsafe { JS_IsExceptionPending(cx) } {
344 throw_dom_exception(cx.into(), global_scope, Error::JSFailed, CanGc::from_cx(cx));
345 false
346 } else {
347 false
348 }
349}
350
351#[expect(unsafe_code)]
352fn is_arguments_object(cx: &mut JSContext, value: HandleValue) -> bool {
354 rooted!(&in(cx) let class_name = unsafe { ToString(cx, value) });
355 let Some(class_name) = NonNull::new(class_name.get()) else {
356 return false;
357 };
358 let class_name = unsafe { jsstr_to_string(cx, class_name) };
359 class_name == "[object Arguments]"
360}
361
362#[derive(Clone, Eq, Hash, PartialEq)]
363struct HashableJSVal(u64);
364
365impl From<HandleValue<'_>> for HashableJSVal {
366 fn from(v: HandleValue<'_>) -> HashableJSVal {
367 HashableJSVal(v.get().asBits_)
368 }
369}
370
371pub(crate) fn jsval_to_webdriver(
373 cx: &mut CurrentRealm,
374 global_scope: &GlobalScope,
375 val: HandleValue,
376) -> WebDriverJSResult {
377 run_a_script::<DomTypeHolder, _>(global_scope, || {
378 let mut seen = HashSet::new();
379 let result = jsval_to_webdriver_inner(cx, global_scope, val, &mut seen);
380
381 if result.is_err() {
382 report_pending_exception(cx);
383 }
384 result
385 })
386}
387
388#[expect(unsafe_code)]
389fn jsval_to_webdriver_inner(
391 cx: &mut CurrentRealm,
392 global_scope: &GlobalScope,
393 val: HandleValue,
394 seen: &mut HashSet<HashableJSVal>,
395) -> WebDriverJSResult {
396 if val.get().is_undefined() {
397 Ok(JSValue::Undefined)
398 } else if val.get().is_null() {
399 Ok(JSValue::Null)
400 } else if val.get().is_boolean() {
401 Ok(JSValue::Boolean(val.get().to_boolean()))
402 } else if val.get().is_number() {
403 Ok(JSValue::Number(val.to_number()))
404 } else if val.get().is_string() {
405 let string = NonNull::new(val.to_string()).expect("Should have a non-Null String");
406 let string = unsafe { jsstr_to_string(cx, string) };
407 Ok(JSValue::String(string))
408 } else if val.get().is_object() {
409 rooted!(&in(cx) let object = match FromJSValConvertible::safe_from_jsval(cx, val, ()).unwrap() {
410 ConversionResult::Success(object) => object,
411 _ => unreachable!(),
412 });
413
414 if let Ok(element) = unsafe { root_from_object::<Element>(*object, cx.raw_cx()) } {
415 if is_stale(&element) {
417 Err(JavaScriptEvaluationError::SerializationError(
418 JavaScriptEvaluationResultSerializationError::StaleElementReference,
419 ))
420 } else {
421 Ok(JSValue::Element(
422 element
423 .upcast::<Node>()
424 .unique_id(element.owner_window().pipeline_id()),
425 ))
426 }
427 } else if let Ok(shadow_root) =
428 unsafe { root_from_object::<ShadowRoot>(*object, cx.raw_cx()) }
429 {
430 if is_detached(&shadow_root) {
432 Err(JavaScriptEvaluationError::SerializationError(
433 JavaScriptEvaluationResultSerializationError::DetachedShadowRoot,
434 ))
435 } else {
436 Ok(JSValue::ShadowRoot(
437 shadow_root
438 .upcast::<Node>()
439 .unique_id(shadow_root.owner_window().pipeline_id()),
440 ))
441 }
442 } else if let Ok(window) = unsafe { root_from_object::<Window>(*object, cx.raw_cx()) } {
443 let window_proxy = window.window_proxy();
444 if window_proxy.is_browsing_context_discarded() {
445 Err(JavaScriptEvaluationError::SerializationError(
446 JavaScriptEvaluationResultSerializationError::StaleElementReference,
447 ))
448 } else if window_proxy.browsing_context_id() == window_proxy.webview_id() {
449 Ok(JSValue::Window(window.webview_id().to_string()))
450 } else {
451 Ok(JSValue::Frame(
452 window_proxy.browsing_context_id().to_string(),
453 ))
454 }
455 } else if object_has_to_json_property(cx, global_scope, object.handle()) {
456 let name = CString::new("toJSON").unwrap();
457 rooted!(&in(cx) let mut value = UndefinedValue());
458 let call_result = unsafe {
459 JS_CallFunctionName(
460 cx,
461 object.handle(),
462 name.as_ptr(),
463 &HandleValueArray::empty(),
464 value.handle_mut(),
465 )
466 };
467
468 if call_result {
469 Ok(jsval_to_webdriver_inner(
470 cx,
471 global_scope,
472 value.handle(),
473 seen,
474 )?)
475 } else {
476 throw_dom_exception(cx.into(), global_scope, Error::JSFailed, CanGc::from_cx(cx));
477 Err(JavaScriptEvaluationError::SerializationError(
478 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
479 ))
480 }
481 } else {
482 clone_an_object(cx, global_scope, val, seen, object.handle())
483 }
484 } else {
485 Err(JavaScriptEvaluationError::SerializationError(
486 JavaScriptEvaluationResultSerializationError::UnknownType,
487 ))
488 }
489}
490
491#[expect(unsafe_code)]
492fn clone_an_object(
494 cx: &mut CurrentRealm,
495 global_scope: &GlobalScope,
496 val: HandleValue,
497 seen: &mut HashSet<HashableJSVal>,
498 object_handle: HandleObject,
499) -> WebDriverJSResult {
500 let hashable = val.into();
501 if seen.contains(&hashable) {
503 return Err(JavaScriptEvaluationError::SerializationError(
504 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
505 ));
506 }
507 seen.insert(hashable.clone());
509
510 let return_val = if is_array_like::<crate::DomTypeHolder>(cx, val) ||
511 is_arguments_object(cx, val)
512 {
513 let mut result: Vec<JSValue> = Vec::new();
514
515 let get_property_result =
516 get_property::<u32>(cx, object_handle, c"length", ConversionBehavior::Default);
517 let length = match get_property_result {
518 Ok(length) => match length {
519 Some(length) => length,
520 _ => {
521 return Err(JavaScriptEvaluationError::SerializationError(
522 JavaScriptEvaluationResultSerializationError::UnknownType,
523 ));
524 },
525 },
526 Err(error) => {
527 throw_dom_exception(cx.into(), global_scope, error, CanGc::from_cx(cx));
528 return Err(JavaScriptEvaluationError::SerializationError(
529 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
530 ));
531 },
532 };
533 for i in 0..length {
535 rooted!(&in(cx) let mut item = UndefinedValue());
536 let cname = CString::new(i.to_string()).unwrap();
537 let get_property_result =
538 get_property_jsval(cx, object_handle, &cname, item.handle_mut());
539 match get_property_result {
540 Ok(_) => {
541 let converted_item =
542 jsval_to_webdriver_inner(cx, global_scope, item.handle(), seen)?;
543
544 result.push(converted_item);
545 },
546 Err(error) => {
547 throw_dom_exception(cx.into(), global_scope, error, CanGc::from_cx(cx));
548 return Err(JavaScriptEvaluationError::SerializationError(
549 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
550 ));
551 },
552 }
553 }
554 Ok(JSValue::Array(result))
555 } else {
556 let mut result = HashMap::new();
557
558 let mut ids = unsafe { IdVector::new(cx.raw_cx()) };
559 let succeeded =
560 unsafe { GetPropertyKeys(cx, object_handle, JSITER_OWNONLY, ids.handle_mut()) };
561 if !succeeded {
562 return Err(JavaScriptEvaluationError::SerializationError(
563 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
564 ));
565 }
566 for id in ids.iter() {
567 rooted!(&in(cx) let id = *id);
568 rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
569
570 let mut is_none = false;
571 let succeeded = unsafe {
572 JS_GetOwnPropertyDescriptorById(
573 cx,
574 object_handle,
575 id.handle(),
576 desc.handle_mut(),
577 &mut is_none,
578 )
579 };
580 if !succeeded {
581 return Err(JavaScriptEvaluationError::SerializationError(
582 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
583 ));
584 }
585
586 rooted!(&in(cx) let mut property = UndefinedValue());
587 let succeeded = unsafe {
588 JS_GetPropertyById(cx, object_handle, id.handle(), property.handle_mut())
589 };
590 if !succeeded {
591 return Err(JavaScriptEvaluationError::SerializationError(
592 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
593 ));
594 }
595
596 if !property.is_undefined() {
597 let name = jsid_to_string(cx, id.handle());
598 let Some(name) = name else {
599 return Err(JavaScriptEvaluationError::SerializationError(
600 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
601 ));
602 };
603
604 let value = jsval_to_webdriver_inner(cx, global_scope, property.handle(), seen)?;
605 result.insert(name.into(), value);
606 }
607 }
608 Ok(JSValue::Object(result))
609 };
610 seen.remove(&hashable);
612 return_val
614}
615
616pub(crate) fn handle_execute_async_script(
617 window: Option<DomRoot<Window>>,
618 eval: String,
619 reply: GenericSender<WebDriverJSResult>,
620 cx: &mut JSContext,
621) {
622 match window {
623 Some(window) => {
624 let reply_sender = reply.clone();
625 window.set_webdriver_script_chan(Some(reply));
626
627 let global_scope = window.as_global_scope();
628
629 let mut realm = enter_auto_realm(cx, global_scope);
630 let mut realm = realm.current_realm();
631 if let Err(error) = global_scope.evaluate_js_on_global(
632 &mut realm,
633 eval.into(),
634 "",
635 None, None,
637 ) {
638 reply_sender.send(Err(error)).unwrap_or_else(|error| {
639 error!("ExecuteAsyncScript Failed to send reply: {error}");
640 });
641 }
642 },
643 None => {
644 reply
645 .send(Err(JavaScriptEvaluationError::DocumentNotFound))
646 .unwrap_or_else(|error| {
647 error!("ExecuteAsyncScript Failed to send reply: {error}");
648 });
649 },
650 }
651}
652
653pub(crate) fn handle_get_parent_frame_id(
655 documents: &DocumentCollection,
656 pipeline: PipelineId,
657 reply: GenericSender<Result<BrowsingContextId, ErrorStatus>>,
658) {
659 reply
662 .send(
663 documents
664 .find_window(pipeline)
665 .and_then(|window| {
666 window
667 .window_proxy()
668 .parent()
669 .map(|parent| parent.browsing_context_id())
670 })
671 .ok_or(ErrorStatus::NoSuchWindow),
672 )
673 .unwrap();
674}
675
676pub(crate) fn handle_get_browsing_context_id(
678 documents: &DocumentCollection,
679 pipeline: PipelineId,
680 webdriver_frame_id: WebDriverFrameId,
681 reply: GenericSender<Result<BrowsingContextId, ErrorStatus>>,
682) {
683 reply
684 .send(match webdriver_frame_id {
685 WebDriverFrameId::Short(id) => {
686 documents
689 .find_document(pipeline)
690 .ok_or(ErrorStatus::NoSuchWindow)
691 .and_then(|document| {
692 document
693 .iframes()
694 .iter()
695 .nth(id as usize)
696 .and_then(|iframe| iframe.browsing_context_id())
697 .ok_or(ErrorStatus::NoSuchFrame)
698 })
699 },
700 WebDriverFrameId::Element(element_id) => {
701 get_known_element(documents, pipeline, element_id).and_then(|element| {
702 element
703 .downcast::<HTMLIFrameElement>()
704 .and_then(|element| element.browsing_context_id())
705 .ok_or(ErrorStatus::NoSuchFrame)
706 })
707 },
708 })
709 .unwrap();
710}
711
712fn get_element_in_view_center_point(cx: &mut JSContext, element: &Element) -> Option<Point2D<i64>> {
714 let doc = element.owner_document();
715 element.GetClientRects(cx).first().map(|rectangle| {
718 let x = rectangle.X();
719 let y = rectangle.Y();
720 let width = rectangle.Width();
721 let height = rectangle.Height();
722 debug!(
723 "get_element_in_view_center_point: Element rectangle at \
724 (x: {x}, y: {y}, width: {width}, height: {height})",
725 );
726 let window = doc.window();
727 let left = (x.min(x + width)).max(0.0);
729 let right = f64::min(window.InnerWidth() as f64, x.max(x + width));
731 let top = (y.min(y + height)).max(0.0);
733 let bottom = f64::min(window.InnerHeight() as f64, y.max(y + height));
736 debug!(
737 "get_element_in_view_center_point: Computed rectangle is \
738 (left: {left}, right: {right}, top: {top}, bottom: {bottom})",
739 );
740 let center_x = ((left + right) / 2.0).floor() as i64;
742 let center_y = ((top + bottom) / 2.0).floor() as i64;
744
745 debug!(
746 "get_element_in_view_center_point: Element center point at ({center_x}, {center_y})",
747 );
748 Point2D::new(center_x, center_y)
750 })
751}
752
753pub(crate) fn handle_get_element_in_view_center_point(
754 cx: &mut JSContext,
755 documents: &DocumentCollection,
756 pipeline: PipelineId,
757 element_id: String,
758 reply: GenericOneshotSender<Result<Option<(i64, i64)>, ErrorStatus>>,
759) {
760 reply
761 .send(
762 get_known_element(documents, pipeline, element_id).map(|element| {
763 get_element_in_view_center_point(cx, &element).map(|point| (point.x, point.y))
764 }),
765 )
766 .unwrap();
767}
768
769fn retrieve_document_and_check_root_existence(
770 documents: &DocumentCollection,
771 pipeline: PipelineId,
772) -> Result<DomRoot<Document>, ErrorStatus> {
773 let document = documents
774 .find_document(pipeline)
775 .ok_or(ErrorStatus::NoSuchWindow)?;
776
777 if document.GetDocumentElement().is_none() {
782 Err(ErrorStatus::NoSuchElement)
783 } else {
784 Ok(document)
785 }
786}
787
788pub(crate) fn handle_find_elements_css_selector(
789 cx: &mut JSContext,
790 documents: &DocumentCollection,
791 pipeline: PipelineId,
792 selector: String,
793 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
794) {
795 match retrieve_document_and_check_root_existence(documents, pipeline) {
796 Ok(document) => reply
797 .send(
798 document
799 .QuerySelectorAll(cx, DOMString::from(selector))
800 .map_err(|_| ErrorStatus::InvalidSelector)
801 .map(|nodes| {
802 nodes
803 .iter()
804 .map(|x| x.upcast::<Node>().unique_id(pipeline))
805 .collect()
806 }),
807 )
808 .unwrap(),
809 Err(error) => reply.send(Err(error)).unwrap(),
810 }
811}
812
813pub(crate) fn handle_find_elements_link_text(
814 cx: &mut JSContext,
815 documents: &DocumentCollection,
816 pipeline: PipelineId,
817 selector: String,
818 partial: bool,
819 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
820) {
821 match retrieve_document_and_check_root_existence(documents, pipeline) {
822 Ok(document) => reply
823 .send(all_matching_links(
824 cx,
825 document.upcast::<Node>(),
826 selector,
827 partial,
828 ))
829 .unwrap(),
830 Err(error) => reply.send(Err(error)).unwrap(),
831 }
832}
833
834pub(crate) fn handle_find_elements_tag_name(
835 cx: &mut JSContext,
836 documents: &DocumentCollection,
837 pipeline: PipelineId,
838 selector: String,
839 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
840) {
841 match retrieve_document_and_check_root_existence(documents, pipeline) {
842 Ok(document) => reply
843 .send(Ok(document
844 .GetElementsByTagName(cx, DOMString::from(selector))
845 .elements_iter()
846 .map(|x| x.upcast::<Node>().unique_id(pipeline))
847 .collect::<Vec<String>>()))
848 .unwrap(),
849 Err(error) => reply.send(Err(error)).unwrap(),
850 }
851}
852
853fn find_elements_xpath_strategy(
855 cx: &mut JSContext,
856 document: &Document,
857 start_node: &Node,
858 selector: String,
859 pipeline: PipelineId,
860) -> Result<Vec<String>, ErrorStatus> {
861 let evaluate_result = match document.Evaluate(
866 cx,
867 DOMString::from(selector),
868 start_node,
869 None,
870 XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE,
871 None,
872 ) {
873 Ok(res) => res,
874 Err(_) => return Err(ErrorStatus::InvalidSelector),
875 };
876 let length = match evaluate_result.GetSnapshotLength() {
882 Ok(len) => len,
883 Err(_) => return Err(ErrorStatus::InvalidSelector),
884 };
885
886 let mut result = Vec::new();
888
889 for index in 0..length {
891 let node = match evaluate_result.SnapshotItem(index) {
894 Ok(node) => node.expect(
895 "Node should always exist as ORDERED_NODE_SNAPSHOT_TYPE \
896 gives static result and we verified the length!",
897 ),
898 Err(_) => return Err(ErrorStatus::InvalidSelector),
899 };
900
901 if !node.is::<Element>() {
903 return Err(ErrorStatus::InvalidSelector);
904 }
905
906 result.push(node.unique_id(pipeline));
908 }
909 Ok(result)
911}
912
913pub(crate) fn handle_find_elements_xpath_selector(
914 cx: &mut JSContext,
915 documents: &DocumentCollection,
916 pipeline: PipelineId,
917 selector: String,
918 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
919) {
920 match retrieve_document_and_check_root_existence(documents, pipeline) {
921 Ok(document) => reply
922 .send(find_elements_xpath_strategy(
923 cx,
924 &document,
925 document.upcast::<Node>(),
926 selector,
927 pipeline,
928 ))
929 .unwrap(),
930 Err(error) => reply.send(Err(error)).unwrap(),
931 }
932}
933
934pub(crate) fn handle_find_element_elements_css_selector(
935 cx: &mut JSContext,
936 documents: &DocumentCollection,
937 pipeline: PipelineId,
938 element_id: String,
939 selector: String,
940 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
941) {
942 reply
943 .send(
944 get_known_element(documents, pipeline, element_id).and_then(|element| {
945 element
946 .upcast::<Node>()
947 .query_selector_all(cx.no_gc(), DOMString::from(selector))
948 .map_err(|_| ErrorStatus::InvalidSelector)
949 .map(|nodes| {
950 nodes
951 .iter()
952 .map(|x| x.upcast::<Node>().unique_id(pipeline))
953 .collect()
954 })
955 }),
956 )
957 .unwrap();
958}
959
960pub(crate) fn handle_find_element_elements_link_text(
961 cx: &mut JSContext,
962 documents: &DocumentCollection,
963 pipeline: PipelineId,
964 element_id: String,
965 selector: String,
966 partial: bool,
967 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
968) {
969 reply
970 .send(
971 get_known_element(documents, pipeline, element_id).and_then(|element| {
972 all_matching_links(cx, element.upcast::<Node>(), selector.clone(), partial)
973 }),
974 )
975 .unwrap();
976}
977
978pub(crate) fn handle_find_element_elements_tag_name(
979 cx: &mut JSContext,
980 documents: &DocumentCollection,
981 pipeline: PipelineId,
982 element_id: String,
983 selector: String,
984 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
985) {
986 reply
987 .send(
988 get_known_element(documents, pipeline, element_id).map(|element| {
989 element
990 .GetElementsByTagName(cx, DOMString::from(selector))
991 .elements_iter()
992 .map(|x| x.upcast::<Node>().unique_id(pipeline))
993 .collect::<Vec<String>>()
994 }),
995 )
996 .unwrap();
997}
998
999pub(crate) fn handle_find_element_elements_xpath_selector(
1000 cx: &mut JSContext,
1001 documents: &DocumentCollection,
1002 pipeline: PipelineId,
1003 element_id: String,
1004 selector: String,
1005 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1006) {
1007 reply
1008 .send(
1009 get_known_element(documents, pipeline, element_id).and_then(|element| {
1010 find_elements_xpath_strategy(
1011 cx,
1012 &documents
1013 .find_document(pipeline)
1014 .expect("Document existence guaranteed by `get_known_element`"),
1015 element.upcast::<Node>(),
1016 selector,
1017 pipeline,
1018 )
1019 }),
1020 )
1021 .unwrap();
1022}
1023
1024pub(crate) fn handle_find_shadow_elements_css_selector(
1026 cx: &mut JSContext,
1027 documents: &DocumentCollection,
1028 pipeline: PipelineId,
1029 shadow_root_id: String,
1030 selector: String,
1031 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1032) {
1033 reply
1034 .send(
1035 get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1036 shadow_root
1037 .upcast::<Node>()
1038 .query_selector_all(cx.no_gc(), DOMString::from(selector))
1039 .map_err(|_| ErrorStatus::InvalidSelector)
1040 .map(|nodes| {
1041 nodes
1042 .iter()
1043 .map(|x| x.upcast::<Node>().unique_id(pipeline))
1044 .collect()
1045 })
1046 }),
1047 )
1048 .unwrap();
1049}
1050
1051pub(crate) fn handle_find_shadow_elements_link_text(
1052 cx: &mut JSContext,
1053 documents: &DocumentCollection,
1054 pipeline: PipelineId,
1055 shadow_root_id: String,
1056 selector: String,
1057 partial: bool,
1058 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1059) {
1060 reply
1061 .send(
1062 get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1063 all_matching_links(cx, shadow_root.upcast::<Node>(), selector.clone(), partial)
1064 }),
1065 )
1066 .unwrap();
1067}
1068
1069pub(crate) fn handle_find_shadow_elements_tag_name(
1070 cx: &mut JSContext,
1071 documents: &DocumentCollection,
1072 pipeline: PipelineId,
1073 shadow_root_id: String,
1074 selector: String,
1075 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1076) {
1077 reply
1083 .send(
1084 get_known_shadow_root(documents, pipeline, shadow_root_id).map(|shadow_root| {
1085 shadow_root
1086 .upcast::<Node>()
1087 .query_selector_all(cx.no_gc(), DOMString::from(selector))
1088 .map(|nodes| {
1089 nodes
1090 .iter()
1091 .map(|x| x.upcast::<Node>().unique_id(pipeline))
1092 .collect()
1093 })
1094 .unwrap_or_default()
1095 }),
1096 )
1097 .unwrap();
1098}
1099
1100pub(crate) fn handle_find_shadow_elements_xpath_selector(
1101 cx: &mut JSContext,
1102 documents: &DocumentCollection,
1103 pipeline: PipelineId,
1104 shadow_root_id: String,
1105 selector: String,
1106 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1107) {
1108 reply
1109 .send(
1110 get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1111 find_elements_xpath_strategy(
1112 cx,
1113 &documents
1114 .find_document(pipeline)
1115 .expect("Document existence guaranteed by `get_known_shadow_root`"),
1116 shadow_root.upcast::<Node>(),
1117 selector,
1118 pipeline,
1119 )
1120 }),
1121 )
1122 .unwrap();
1123}
1124
1125pub(crate) fn handle_get_element_shadow_root(
1127 documents: &DocumentCollection,
1128 pipeline: PipelineId,
1129 element_id: String,
1130 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1131) {
1132 reply
1133 .send(
1134 get_known_element(documents, pipeline, element_id).map(|element| {
1135 element
1136 .shadow_root()
1137 .map(|x| x.upcast::<Node>().unique_id(pipeline))
1138 }),
1139 )
1140 .unwrap();
1141}
1142
1143impl Element {
1144 fn is_keyboard_interactable(&self) -> bool {
1146 self.is_focusable_area() || self.is::<HTMLBodyElement>() || self.is_document_element()
1147 }
1148}
1149
1150fn handle_send_keys_file(
1151 file_input: &HTMLInputElement,
1152 text: &str,
1153 reply_sender: GenericSender<Result<bool, ErrorStatus>>,
1154) {
1155 let files: Vec<DOMString> = text
1160 .split("\n")
1161 .filter_map(|string| {
1162 if string.is_empty() {
1163 None
1164 } else {
1165 Some(string.into())
1166 }
1167 })
1168 .collect();
1169
1170 if files.is_empty() {
1172 let _ = reply_sender.send(Err(ErrorStatus::InvalidArgument));
1173 return;
1174 }
1175
1176 if !file_input.Multiple() && files.len() > 1 {
1180 let _ = reply_sender.send(Err(ErrorStatus::InvalidArgument));
1181 return;
1182 }
1183
1184 file_input.select_files_for_webdriver(files, reply_sender);
1193}
1194
1195fn handle_send_keys_non_typeable(
1197 cx: &mut JSContext,
1198 input_element: &HTMLInputElement,
1199 text: &str,
1200) -> Result<bool, ErrorStatus> {
1201 if !input_element.is_mutable() {
1208 return Err(ErrorStatus::ElementNotInteractable);
1209 }
1210
1211 if let Err(error) = input_element.SetValue(cx, text.into()) {
1213 error!(
1214 "Failed to set value on non-typeable input element: {:?}",
1215 error
1216 );
1217 return Err(ErrorStatus::UnknownError);
1218 }
1219
1220 if input_element
1222 .Validity(cx)
1223 .invalid_flags()
1224 .contains(ValidationFlags::BAD_INPUT)
1225 {
1226 return Err(ErrorStatus::InvalidArgument);
1227 }
1228
1229 Ok(false)
1232}
1233
1234pub(crate) fn handle_will_send_keys(
1240 cx: &mut JSContext,
1241 documents: &DocumentCollection,
1242 pipeline: PipelineId,
1243 element_id: String,
1244 text: String,
1245 strict_file_interactability: bool,
1246 reply: GenericSender<Result<bool, ErrorStatus>>,
1247) {
1248 let element = match get_known_element(documents, pipeline, element_id) {
1250 Ok(element) => element,
1251 Err(error) => {
1252 let _ = reply.send(Err(error));
1253 return;
1254 },
1255 };
1256
1257 let input_element = element.downcast::<HTMLInputElement>();
1258 let mut element_has_focus = false;
1259
1260 let is_file_input =
1263 input_element.is_some_and(|e| matches!(*e.input_type(), InputType::File(_)));
1264
1265 if !is_file_input || strict_file_interactability {
1267 scroll_into_view(cx, &element, documents, &pipeline);
1269
1270 if !element.is_keyboard_interactable() {
1276 let _ = reply.send(Err(ErrorStatus::ElementNotInteractable));
1277 return;
1278 }
1279
1280 let Some(html_element) = element.downcast::<HTMLElement>() else {
1283 let _ = reply.send(Err(ErrorStatus::UnknownError));
1284 return;
1285 };
1286
1287 if !element.is_active_element() {
1288 html_element.Focus(
1289 cx,
1290 &FocusOptions {
1291 preventScroll: true,
1292 },
1293 );
1294 } else {
1295 element_has_focus = element.focus_state();
1296 }
1297 }
1298
1299 if let Some(input_element) = input_element {
1300 if is_file_input {
1302 handle_send_keys_file(input_element, &text, reply);
1303 return;
1304 }
1305
1306 if input_element.is_nontypeable() {
1308 let _ = reply.send(handle_send_keys_non_typeable(cx, input_element, &text));
1309 return;
1310 }
1311 }
1312
1313 if !element_has_focus {
1321 if let Some(input_element) = input_element {
1322 let length = input_element.Value().len() as u32;
1323 let _ = input_element.SetSelectionRange(length, length, None);
1324 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1325 let length = textarea_element.Value().len() as u32;
1326 let _ = textarea_element.SetSelectionRange(length, length, None);
1327 }
1328 }
1329
1330 let _ = reply.send(Ok(true));
1331}
1332
1333pub(crate) fn handle_get_active_element(
1334 documents: &DocumentCollection,
1335 pipeline: PipelineId,
1336 reply: GenericSender<Option<String>>,
1337) {
1338 reply
1339 .send(
1340 documents
1341 .find_document(pipeline)
1342 .and_then(|document| document.GetActiveElement())
1343 .map(|element| element.upcast::<Node>().unique_id(pipeline)),
1344 )
1345 .unwrap();
1346}
1347
1348pub(crate) fn handle_get_computed_role(
1349 documents: &DocumentCollection,
1350 pipeline: PipelineId,
1351 node_id: String,
1352 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1353) {
1354 reply
1355 .send(
1356 get_known_element(documents, pipeline, node_id)
1357 .map(|element| element.GetRole().map(String::from)),
1361 )
1362 .unwrap();
1363}
1364
1365pub(crate) fn handle_get_page_source(
1366 cx: &mut JSContext,
1367 documents: &DocumentCollection,
1368 pipeline: PipelineId,
1369 reply: GenericSender<Result<String, ErrorStatus>>,
1370) {
1371 reply
1372 .send(
1373 documents
1374 .find_document(pipeline)
1375 .ok_or(ErrorStatus::UnknownError)
1376 .and_then(|document| match document.GetDocumentElement() {
1377 Some(element) => match element.outer_html(cx) {
1378 Ok(source) => Ok(String::from(source)),
1379 Err(_) => {
1380 match XMLSerializer::new(document.window(), None, CanGc::from_cx(cx))
1381 .SerializeToString(element.upcast::<Node>())
1382 {
1383 Ok(source) => Ok(String::from(source)),
1384 Err(_) => Err(ErrorStatus::UnknownError),
1385 }
1386 },
1387 },
1388 None => Err(ErrorStatus::UnknownError),
1389 }),
1390 )
1391 .unwrap();
1392}
1393
1394pub(crate) fn handle_get_cookies(
1395 documents: &DocumentCollection,
1396 pipeline: PipelineId,
1397 reply: GenericSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
1398) {
1399 reply
1400 .send(
1401 match documents.find_document(pipeline) {
1403 Some(document) => {
1404 let url = document.url();
1405 let (sender, receiver) = generic_channel::channel().unwrap();
1406 let _ = document
1407 .window()
1408 .as_global_scope()
1409 .resource_threads()
1410 .send(GetCookiesForUrl(url, sender, NonHTTP));
1411 Ok(receiver.recv().unwrap())
1412 },
1413 None => Ok(Vec::new()),
1414 },
1415 )
1416 .unwrap();
1417}
1418
1419pub(crate) fn handle_get_cookie(
1421 documents: &DocumentCollection,
1422 pipeline: PipelineId,
1423 name: String,
1424 reply: GenericSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
1425) {
1426 reply
1427 .send(
1428 match documents.find_document(pipeline) {
1430 Some(document) => {
1431 let url = document.url();
1432 let (sender, receiver) = generic_channel::channel().unwrap();
1433 let _ = document
1434 .window()
1435 .as_global_scope()
1436 .resource_threads()
1437 .send(GetCookiesForUrl(url, sender, NonHTTP));
1438 let cookies = receiver.recv().unwrap();
1439 Ok(cookies
1440 .into_iter()
1441 .filter(|cookie| cookie.name() == &*name)
1442 .collect())
1443 },
1444 None => Ok(Vec::new()),
1445 },
1446 )
1447 .unwrap();
1448}
1449
1450pub(crate) fn handle_add_cookie(
1452 documents: &DocumentCollection,
1453 pipeline: PipelineId,
1454 cookie: Cookie<'static>,
1455 reply: GenericSender<Result<(), ErrorStatus>>,
1456) {
1457 let document = match documents.find_document(pipeline) {
1459 Some(document) => document,
1460 None => {
1461 return reply.send(Err(ErrorStatus::NoSuchWindow)).unwrap();
1462 },
1463 };
1464 let url = document.url();
1465 let method = if cookie.http_only().unwrap_or(false) {
1466 HTTP
1467 } else {
1468 NonHTTP
1469 };
1470
1471 let domain = cookie.domain().map(ToOwned::to_owned);
1472 reply
1474 .send(match (document.is_cookie_averse(), domain) {
1475 (true, _) => Err(ErrorStatus::InvalidCookieDomain),
1478 (false, Some(ref domain)) if url.host_str().is_some_and(|host| host == domain) => {
1479 let _ = document
1480 .window()
1481 .as_global_scope()
1482 .resource_threads()
1483 .send(SetCookieForUrl(url, Serde(cookie), method, None));
1484 Ok(())
1485 },
1486 (false, Some(_)) => Err(ErrorStatus::InvalidCookieDomain),
1489 (false, None) => {
1490 let _ = document
1491 .window()
1492 .as_global_scope()
1493 .resource_threads()
1494 .send(SetCookieForUrl(url, Serde(cookie), method, None));
1495 Ok(())
1496 },
1497 })
1498 .unwrap();
1499}
1500
1501pub(crate) fn handle_delete_cookies(
1503 documents: &DocumentCollection,
1504 pipeline: PipelineId,
1505 reply: GenericSender<Result<(), ErrorStatus>>,
1506) {
1507 let document = match documents.find_document(pipeline) {
1508 Some(document) => document,
1509 None => {
1510 return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
1511 },
1512 };
1513 let url = document.url();
1514 document
1515 .window()
1516 .as_global_scope()
1517 .resource_threads()
1518 .send(DeleteCookies(Some(url), None))
1519 .unwrap();
1520 reply.send(Ok(())).unwrap();
1521}
1522
1523pub(crate) fn handle_delete_cookie(
1525 documents: &DocumentCollection,
1526 pipeline: PipelineId,
1527 name: String,
1528 reply: GenericSender<Result<(), ErrorStatus>>,
1529) {
1530 let document = match documents.find_document(pipeline) {
1531 Some(document) => document,
1532 None => {
1533 return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
1534 },
1535 };
1536 let url = document.url();
1537 document
1538 .window()
1539 .as_global_scope()
1540 .resource_threads()
1541 .send(DeleteCookie(url, name))
1542 .unwrap();
1543 reply.send(Ok(())).unwrap();
1544}
1545
1546pub(crate) fn handle_get_title(
1547 documents: &DocumentCollection,
1548 pipeline: PipelineId,
1549 reply: GenericSender<String>,
1550) {
1551 reply
1552 .send(
1553 documents
1555 .find_document(pipeline)
1556 .map(|document| String::from(document.Title()))
1557 .unwrap_or_default(),
1558 )
1559 .unwrap();
1560}
1561
1562fn calculate_absolute_position(
1564 documents: &DocumentCollection,
1565 pipeline: &PipelineId,
1566 rect: &DOMRect,
1567) -> Result<(f64, f64), ErrorStatus> {
1568 let document = match documents.find_document(*pipeline) {
1573 Some(document) => document,
1574 None => return Err(ErrorStatus::UnknownError),
1575 };
1576 let win = match document.GetDefaultView() {
1577 Some(win) => win,
1578 None => return Err(ErrorStatus::UnknownError),
1579 };
1580
1581 let x = win.ScrollX() as f64 + rect.X();
1583 let y = win.ScrollY() as f64 + rect.Y();
1584
1585 Ok((x, y))
1586}
1587
1588pub(crate) fn handle_get_rect(
1590 cx: &mut JSContext,
1591 documents: &DocumentCollection,
1592 pipeline: PipelineId,
1593 element_id: String,
1594 reply: GenericSender<Result<Rect<f64>, ErrorStatus>>,
1595) {
1596 reply
1597 .send(
1598 get_known_element(documents, pipeline, element_id).and_then(|element| {
1599 let rect = element.GetBoundingClientRect(cx);
1603 let (x, y) = calculate_absolute_position(documents, &pipeline, &rect)?;
1604
1605 Ok(Rect::new(
1607 Point2D::new(x, y),
1608 Size2D::new(rect.Width(), rect.Height()),
1609 ))
1610 }),
1611 )
1612 .unwrap();
1613}
1614
1615pub(crate) fn handle_scroll_and_get_bounding_client_rect(
1616 cx: &mut JSContext,
1617 documents: &DocumentCollection,
1618 pipeline: PipelineId,
1619 element_id: String,
1620 reply: GenericSender<Result<Rect<f32>, ErrorStatus>>,
1621) {
1622 reply
1623 .send(
1624 get_known_element(documents, pipeline, element_id).map(|element| {
1625 scroll_into_view(cx, &element, documents, &pipeline);
1626
1627 let rect = element.GetBoundingClientRect(cx);
1628 Rect::new(
1629 Point2D::new(rect.X() as f32, rect.Y() as f32),
1630 Size2D::new(rect.Width() as f32, rect.Height() as f32),
1631 )
1632 }),
1633 )
1634 .unwrap();
1635}
1636
1637pub(crate) fn handle_get_text(
1639 documents: &DocumentCollection,
1640 pipeline: PipelineId,
1641 node_id: String,
1642 reply: GenericSender<Result<String, ErrorStatus>>,
1643) {
1644 reply
1645 .send(
1646 get_known_element(documents, pipeline, node_id).map(|element| {
1647 element
1648 .downcast::<HTMLElement>()
1649 .map(|htmlelement| String::from(htmlelement.InnerText()))
1650 .unwrap_or_else(|| {
1651 element
1652 .upcast::<Node>()
1653 .GetTextContent()
1654 .map_or("".to_owned(), String::from)
1655 })
1656 }),
1657 )
1658 .unwrap();
1659}
1660
1661pub(crate) fn handle_get_name(
1662 documents: &DocumentCollection,
1663 pipeline: PipelineId,
1664 node_id: String,
1665 reply: GenericSender<Result<String, ErrorStatus>>,
1666) {
1667 reply
1668 .send(
1669 get_known_element(documents, pipeline, node_id)
1670 .map(|element| String::from(element.TagName())),
1671 )
1672 .unwrap();
1673}
1674
1675pub(crate) fn handle_get_attribute(
1676 cx: &mut JSContext,
1677 documents: &DocumentCollection,
1678 pipeline: PipelineId,
1679 node_id: String,
1680 name: String,
1681 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1682) {
1683 reply
1684 .send(
1685 get_known_element(documents, pipeline, node_id).map(|element| {
1686 if is_boolean_attribute(&name) {
1687 if element.HasAttribute(cx, DOMString::from(name)) {
1688 Some(String::from("true"))
1689 } else {
1690 None
1691 }
1692 } else {
1693 element
1694 .GetAttribute(cx, DOMString::from(name))
1695 .map(String::from)
1696 }
1697 }),
1698 )
1699 .unwrap();
1700}
1701
1702pub(crate) fn handle_get_property(
1703 documents: &DocumentCollection,
1704 pipeline: PipelineId,
1705 node_id: String,
1706 name: String,
1707 reply: GenericSender<Result<JSValue, ErrorStatus>>,
1708 cx: &mut JSContext,
1709) {
1710 reply
1711 .send(
1712 get_known_element(documents, pipeline, node_id).map(|element| {
1713 let document = documents.find_document(pipeline).unwrap();
1714
1715 let Ok(cname) = CString::new(name) else {
1716 return JSValue::Undefined;
1717 };
1718
1719 let mut realm = enter_auto_realm(cx, &*document);
1720 let cx = &mut realm.current_realm();
1721
1722 rooted!(&in(cx) let mut property = UndefinedValue());
1723 match get_property_jsval(
1724 cx,
1725 element.reflector().get_jsobject(),
1726 &cname,
1727 property.handle_mut(),
1728 ) {
1729 Ok(_) => match jsval_to_webdriver(cx, &element.global(), property.handle()) {
1730 Ok(property) => property,
1731 Err(_) => JSValue::Undefined,
1732 },
1733 Err(error) => {
1734 throw_dom_exception(
1735 cx.into(),
1736 &element.global(),
1737 error,
1738 CanGc::from_cx(cx),
1739 );
1740 JSValue::Undefined
1741 },
1742 }
1743 }),
1744 )
1745 .unwrap();
1746}
1747
1748pub(crate) fn handle_get_css(
1749 cx: &mut JSContext,
1750 documents: &DocumentCollection,
1751 pipeline: PipelineId,
1752 node_id: String,
1753 name: String,
1754 reply: GenericSender<Result<String, ErrorStatus>>,
1755) {
1756 reply
1757 .send(
1758 get_known_element(documents, pipeline, node_id).map(|element| {
1759 let window = element.owner_window();
1760 String::from(
1761 window
1762 .GetComputedStyle(cx, &element, None)
1763 .GetPropertyValue(DOMString::from(name)),
1764 )
1765 }),
1766 )
1767 .unwrap();
1768}
1769
1770pub(crate) fn handle_get_url(
1771 documents: &DocumentCollection,
1772 pipeline: PipelineId,
1773 reply: GenericSender<String>,
1774) {
1775 reply
1776 .send(
1777 documents
1779 .find_document(pipeline)
1780 .map(|document| document.url().into_string())
1781 .unwrap_or_else(|| "about:blank".to_string()),
1782 )
1783 .unwrap();
1784}
1785
1786fn element_is_mutable_form_control(element: &Element) -> bool {
1788 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1789 input_element.is_mutable() &&
1790 matches!(
1791 *input_element.input_type(),
1792 InputType::Text(_) |
1793 InputType::Search(_) |
1794 InputType::Url(_) |
1795 InputType::Tel(_) |
1796 InputType::Email(_) |
1797 InputType::Password(_) |
1798 InputType::Date(_) |
1799 InputType::Month(_) |
1800 InputType::Week(_) |
1801 InputType::Time(_) |
1802 InputType::DatetimeLocal(_) |
1803 InputType::Number(_) |
1804 InputType::Range(_) |
1805 InputType::Color(_) |
1806 InputType::File(_)
1807 )
1808 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1809 textarea_element.is_mutable()
1810 } else {
1811 false
1812 }
1813}
1814
1815fn clear_a_resettable_element(cx: &mut JSContext, element: &Element) -> Result<(), ErrorStatus> {
1817 let html_element = element
1818 .downcast::<HTMLElement>()
1819 .ok_or(ErrorStatus::UnknownError)?;
1820
1821 if html_element.is_candidate_for_constraint_validation() {
1824 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1825 if input_element.Value().is_empty() {
1826 return Ok(());
1827 }
1828 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() &&
1829 textarea_element.Value().is_empty()
1830 {
1831 return Ok(());
1832 }
1833 }
1834
1835 html_element.Focus(
1837 cx,
1838 &FocusOptions {
1839 preventScroll: true,
1840 },
1841 );
1842
1843 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1845 input_element.clear(cx);
1846 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1847 textarea_element.clear();
1848 } else {
1849 unreachable!("We have confirm previously that element is mutable form control");
1850 }
1851
1852 let event_target = element.upcast::<EventTarget>();
1853 event_target.fire_bubbling_event(cx, atom!("input"));
1854 event_target.fire_bubbling_event(cx, atom!("change"));
1855
1856 html_element.Blur(cx);
1858
1859 Ok(())
1860}
1861
1862pub(crate) fn handle_element_clear(
1864 cx: &mut JSContext,
1865 documents: &DocumentCollection,
1866 pipeline: PipelineId,
1867 element_id: String,
1868 reply: GenericSender<Result<(), ErrorStatus>>,
1869) {
1870 reply
1871 .send(
1872 get_known_element(documents, pipeline, element_id).and_then(|element| {
1873 if !element_is_mutable_form_control(&element) {
1877 return Err(ErrorStatus::InvalidElementState);
1878 }
1879
1880 scroll_into_view(cx, &element, documents, &pipeline);
1882
1883 if !element.is_keyboard_interactable() {
1889 return Err(ErrorStatus::ElementNotInteractable);
1890 }
1891
1892 let paint_tree = get_element_pointer_interactable_paint_tree(
1893 cx,
1894 &element,
1895 &documents
1896 .find_document(pipeline)
1897 .expect("Document existence guaranteed by `get_known_element`"),
1898 );
1899 if !is_element_in_view(&element, &paint_tree) {
1900 return Err(ErrorStatus::ElementNotInteractable);
1901 }
1902
1903 clear_a_resettable_element(cx, &element)
1906 }),
1907 )
1908 .unwrap();
1909}
1910
1911fn get_option_parent(node: &Node) -> Option<DomRoot<Element>> {
1912 let mut candidate_select = None;
1920
1921 for ancestor in node.ancestors() {
1922 if ancestor.is::<HTMLDataListElement>() {
1923 return Some(DomRoot::downcast::<Element>(ancestor).unwrap());
1924 } else if candidate_select.is_none() && ancestor.is::<HTMLSelectElement>() {
1925 candidate_select = Some(ancestor);
1926 }
1927 }
1928
1929 candidate_select.map(|ancestor| DomRoot::downcast::<Element>(ancestor).unwrap())
1930}
1931
1932fn get_container(element: &Element) -> Option<DomRoot<Element>> {
1934 if element.is::<HTMLOptionElement>() {
1935 return get_option_parent(element.upcast::<Node>());
1936 }
1937 if element.is::<HTMLOptGroupElement>() {
1938 return get_option_parent(element.upcast::<Node>())
1939 .or_else(|| Some(DomRoot::from_ref(element)));
1940 }
1941 Some(DomRoot::from_ref(element))
1942}
1943
1944pub(crate) fn handle_element_click(
1946 cx: &mut JSContext,
1947 documents: &DocumentCollection,
1948 pipeline: PipelineId,
1949 element_id: String,
1950 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1951) {
1952 reply
1953 .send(
1954 get_known_element(documents, pipeline, element_id).and_then(|element| {
1956 if let Some(input_element) = element.downcast::<HTMLInputElement>() &&
1959 matches!(*input_element.input_type(), InputType::File(_))
1960 {
1961 return Err(ErrorStatus::InvalidArgument);
1962 }
1963
1964 let Some(container) = get_container(&element) else {
1965 return Err(ErrorStatus::UnknownError);
1966 };
1967
1968 scroll_into_view(cx, &container, documents, &pipeline);
1970
1971 let paint_tree = get_element_pointer_interactable_paint_tree(
1974 cx,
1975 &container,
1976 &documents
1977 .find_document(pipeline)
1978 .expect("Document existence guaranteed by `get_known_element`"),
1979 );
1980
1981 if !is_element_in_view(&container, &paint_tree) {
1982 return Err(ErrorStatus::ElementNotInteractable);
1983 }
1984
1985 if !container
1992 .upcast::<Node>()
1993 .is_shadow_including_inclusive_ancestor_of(paint_tree[0].upcast::<Node>())
1994 {
1995 return Err(ErrorStatus::ElementClickIntercepted);
1996 }
1997
1998 match element.downcast::<HTMLOptionElement>() {
2000 Some(option_element) => {
2001 let event_target = container.upcast::<EventTarget>();
2003 event_target.fire_event(cx, atom!("mouseover"));
2004 event_target.fire_event(cx, atom!("mousemove"));
2005 event_target.fire_event(cx, atom!("mousedown"));
2006
2007 match container.downcast::<HTMLElement>() {
2009 Some(html_element) => {
2010 html_element.Focus(
2011 cx,
2012 &FocusOptions {
2013 preventScroll: true,
2014 },
2015 );
2016 },
2017 None => return Err(ErrorStatus::UnknownError),
2018 }
2019
2020 if !is_disabled(&element) {
2022 event_target.fire_event(cx, atom!("input"));
2024
2025 let previous_selectedness = option_element.Selected();
2027
2028 match container.downcast::<HTMLSelectElement>() {
2030 Some(select_element) => {
2031 if select_element.Multiple() {
2032 option_element.SetSelected(cx, !option_element.Selected());
2033 }
2034 },
2035 None => option_element.SetSelected(cx, true),
2036 }
2037
2038 if !previous_selectedness {
2040 event_target.fire_event(cx, atom!("change"));
2041 }
2042 }
2043
2044 event_target.fire_event(cx, atom!("mouseup"));
2046 event_target.fire_event(cx, atom!("click"));
2047
2048 Ok(None)
2049 },
2050 None => Ok(Some(element.upcast::<Node>().unique_id(pipeline))),
2051 }
2052 }),
2053 )
2054 .unwrap();
2055}
2056
2057fn is_element_in_view(element: &Element, paint_tree: &[DomRoot<Element>]) -> bool {
2059 if !paint_tree.contains(&DomRoot::from_ref(element)) {
2062 return false;
2063 }
2064 use style::computed_values::pointer_events::T as PointerEvents;
2065 element
2069 .style()
2070 .is_none_or(|style| style.get_inherited_ui().pointer_events != PointerEvents::None)
2071}
2072
2073fn get_element_pointer_interactable_paint_tree(
2075 cx: &mut JSContext,
2076 element: &Element,
2077 document: &Document,
2078) -> Vec<DomRoot<Element>> {
2079 if !element.is_connected() {
2082 return Vec::new();
2083 }
2084
2085 get_element_in_view_center_point(cx, element).map_or(Vec::new(), |center_point| {
2091 document.ElementsFromPoint(
2092 Finite::wrap(center_point.x as f64),
2093 Finite::wrap(center_point.y as f64),
2094 )
2095 })
2096}
2097
2098pub(crate) fn handle_is_enabled(
2100 documents: &DocumentCollection,
2101 pipeline: PipelineId,
2102 element_id: String,
2103 reply: GenericSender<Result<bool, ErrorStatus>>,
2104) {
2105 reply
2106 .send(
2107 get_known_element(documents, pipeline, element_id).map(|element| {
2109 let document = documents.find_document(pipeline).unwrap();
2111
2112 if document.is_html_document() || document.is_xhtml_document() {
2118 !is_disabled(&element)
2119 } else {
2120 false
2121 }
2122 }),
2123 )
2124 .unwrap();
2125}
2126
2127pub(crate) fn handle_is_selected(
2128 documents: &DocumentCollection,
2129 pipeline: PipelineId,
2130 element_id: String,
2131 reply: GenericSender<Result<bool, ErrorStatus>>,
2132) {
2133 reply
2134 .send(
2135 get_known_element(documents, pipeline, element_id).and_then(|element| {
2136 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
2137 Ok(input_element.Checked())
2138 } else if let Some(option_element) = element.downcast::<HTMLOptionElement>() {
2139 Ok(option_element.Selected())
2140 } else if element.is::<HTMLElement>() {
2141 Ok(false) } else {
2143 Err(ErrorStatus::UnknownError)
2144 }
2145 }),
2146 )
2147 .unwrap();
2148}
2149
2150pub(crate) fn handle_add_load_status_sender(
2151 documents: &DocumentCollection,
2152 pipeline: PipelineId,
2153 reply: GenericSender<WebDriverLoadStatus>,
2154) {
2155 if let Some(document) = documents.find_document(pipeline) {
2156 let window = document.window();
2157 window.set_webdriver_load_status_sender(Some(reply));
2158 }
2159}
2160
2161pub(crate) fn handle_remove_load_status_sender(
2162 documents: &DocumentCollection,
2163 pipeline: PipelineId,
2164) {
2165 if let Some(document) = documents.find_document(pipeline) {
2166 let window = document.window();
2167 window.set_webdriver_load_status_sender(None);
2168 }
2169}
2170
2171fn scroll_into_view(
2173 cx: &mut JSContext,
2174 element: &Element,
2175 documents: &DocumentCollection,
2176 pipeline: &PipelineId,
2177) {
2178 let paint_tree = get_element_pointer_interactable_paint_tree(
2180 cx,
2181 element,
2182 &documents
2183 .find_document(*pipeline)
2184 .expect("Document existence guaranteed by `get_known_element`"),
2185 );
2186 if is_element_in_view(element, &paint_tree) {
2187 return;
2188 }
2189
2190 let options = BooleanOrScrollIntoViewOptions::ScrollIntoViewOptions(ScrollIntoViewOptions {
2195 parent: ScrollOptions {
2196 behavior: ScrollBehavior::Instant,
2197 },
2198 block: ScrollLogicalPosition::End,
2199 inline: ScrollLogicalPosition::Nearest,
2200 container: Default::default(),
2201 });
2202 element.ScrollIntoView(cx, options);
2204}
2205
2206pub(crate) fn set_protocol_handler_automation_mode(
2207 documents: &DocumentCollection,
2208 pipeline: PipelineId,
2209 mode: CustomHandlersAutomationMode,
2210) {
2211 if let Some(document) = documents.find_document(pipeline) {
2212 document.set_protocol_handler_automation_mode(mode);
2213 }
2214}