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, NoGC};
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::form_controls::htmlinputelement::HTMLInputElement;
77use crate::dom::html::form_controls::input_type::InputType;
78use crate::dom::html::htmlbodyelement::HTMLBodyElement;
79use crate::dom::html::htmldatalistelement::HTMLDataListElement;
80use crate::dom::html::htmlelement::HTMLElement;
81use crate::dom::html::htmlformelement::FormControl;
82use crate::dom::html::htmliframeelement::HTMLIFrameElement;
83use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement;
84use crate::dom::html::htmloptionelement::HTMLOptionElement;
85use crate::dom::html::htmlselectelement::HTMLSelectElement;
86use crate::dom::html::htmltextareaelement::HTMLTextAreaElement;
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_thread::ScriptThread;
96
97fn is_stale(element: &Element) -> bool {
99 !element.owner_document().is_active() || !element.is_connected()
102}
103
104fn is_detached(shadow_root: &ShadowRoot) -> bool {
106 !shadow_root.owner_document().is_active() || is_stale(&shadow_root.Host())
109}
110
111fn is_disabled(element: &Element) -> bool {
113 if element.is::<HTMLOptionElement>() || element.is::<HTMLOptGroupElement>() {
115 let disabled = element
117 .upcast::<Node>()
118 .inclusive_ancestors(ShadowIncluding::No)
119 .any(|node| {
120 if node.is::<HTMLOptGroupElement>() || node.is::<HTMLSelectElement>() {
121 node.downcast::<Element>().unwrap().is_actually_disabled()
124 } else {
125 false
126 }
127 });
128
129 if disabled {
134 return true;
135 }
136 }
137 element.is_actually_disabled()
139}
140
141pub(crate) fn handle_get_known_window(
142 documents: &DocumentCollection,
143 pipeline: PipelineId,
144 webview_id: String,
145 reply: GenericSender<Result<(), ErrorStatus>>,
146) {
147 if reply
148 .send(
149 documents
150 .find_window(pipeline)
151 .map_or(Err(ErrorStatus::NoSuchWindow), |window| {
152 let window_proxy = window.window_proxy();
153 if window_proxy.browsing_context_id() != window_proxy.webview_id() ||
155 window_proxy.webview_id().to_string() != webview_id
156 {
157 Err(ErrorStatus::NoSuchWindow)
158 } else {
159 Ok(())
160 }
161 }),
162 )
163 .is_err()
164 {
165 error!("Webdriver get known window reply failed");
166 }
167}
168
169pub(crate) fn handle_get_known_shadow_root(
170 documents: &DocumentCollection,
171 pipeline: PipelineId,
172 shadow_root_id: String,
173 reply: GenericSender<Result<(), ErrorStatus>>,
174) {
175 let result = get_known_shadow_root(documents, pipeline, shadow_root_id).map(|_| ());
176 if reply.send(result).is_err() {
177 error!("Webdriver get known shadow root reply failed");
178 }
179}
180
181fn get_known_shadow_root(
183 documents: &DocumentCollection,
184 pipeline: PipelineId,
185 node_id: String,
186) -> Result<DomRoot<ShadowRoot>, ErrorStatus> {
187 let doc = documents
188 .find_document(pipeline)
189 .ok_or(ErrorStatus::NoSuchWindow)?;
190 if !ScriptThread::has_node_id(pipeline, &node_id) {
193 return Err(ErrorStatus::NoSuchShadowRoot);
194 }
195
196 let node = find_node_by_unique_id_in_document(&doc, node_id);
199
200 if let Some(ref node) = node &&
203 !node.is::<ShadowRoot>()
204 {
205 return Err(ErrorStatus::NoSuchShadowRoot);
206 }
207
208 let Some(node) = node else {
210 return Err(ErrorStatus::DetachedShadowRoot);
211 };
212
213 let shadow_root = DomRoot::downcast::<ShadowRoot>(node).unwrap();
217 if is_detached(&shadow_root) {
218 return Err(ErrorStatus::DetachedShadowRoot);
219 }
220 Ok(shadow_root)
222}
223
224pub(crate) fn handle_get_known_element(
225 documents: &DocumentCollection,
226 pipeline: PipelineId,
227 element_id: String,
228 reply: GenericSender<Result<(), ErrorStatus>>,
229) {
230 let result = get_known_element(documents, pipeline, element_id).map(|_| ());
231 if reply.send(result).is_err() {
232 error!("Webdriver get known element reply failed");
233 }
234}
235
236fn get_known_element(
238 documents: &DocumentCollection,
239 pipeline: PipelineId,
240 node_id: String,
241) -> Result<DomRoot<Element>, ErrorStatus> {
242 let doc = documents
243 .find_document(pipeline)
244 .ok_or(ErrorStatus::NoSuchWindow)?;
245 if !ScriptThread::has_node_id(pipeline, &node_id) {
248 return Err(ErrorStatus::NoSuchElement);
249 }
250 let node = find_node_by_unique_id_in_document(&doc, node_id);
253
254 if let Some(ref node) = node &&
257 !node.is::<Element>()
258 {
259 return Err(ErrorStatus::NoSuchElement);
260 }
261 let Some(node) = node else {
263 return Err(ErrorStatus::StaleElementReference);
264 };
265 let element = DomRoot::downcast::<Element>(node).unwrap();
267 if is_stale(&element) {
268 return Err(ErrorStatus::StaleElementReference);
269 }
270 Ok(element)
272}
273
274pub(crate) fn find_node_by_unique_id_in_document(
276 document: &Document,
277 node_id: String,
278) -> Option<DomRoot<Node>> {
279 let pipeline = document.window().pipeline_id();
280 document
281 .upcast::<Node>()
282 .traverse_preorder(ShadowIncluding::Yes)
283 .find(|node| node.unique_id(pipeline) == node_id)
284}
285
286fn matching_links<'a>(
288 cx: &'a NoGC,
289 links: &'a NodeList,
290 link_text: String,
291 partial: bool,
292) -> impl Iterator<Item = String> + 'a {
293 links
294 .iter(cx)
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, DOMString::from("a"))
322 .map_err(|_| ErrorStatus::InvalidSelector)
323 .map(|nodes| matching_links(cx, &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, global_scope, Error::JSFailed);
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, global_scope, Error::JSFailed);
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, _, _>(cx, global_scope, |cx| {
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>(cx, *object) } {
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) = unsafe { root_from_object::<ShadowRoot>(cx, *object) } {
428 if is_detached(&shadow_root) {
430 Err(JavaScriptEvaluationError::SerializationError(
431 JavaScriptEvaluationResultSerializationError::DetachedShadowRoot,
432 ))
433 } else {
434 Ok(JSValue::ShadowRoot(
435 shadow_root
436 .upcast::<Node>()
437 .unique_id(shadow_root.owner_window().pipeline_id()),
438 ))
439 }
440 } else if let Ok(window) = unsafe { root_from_object::<Window>(cx, *object) } {
441 let window_proxy = window.window_proxy();
442 if window_proxy.is_browsing_context_discarded() {
443 Err(JavaScriptEvaluationError::SerializationError(
444 JavaScriptEvaluationResultSerializationError::StaleElementReference,
445 ))
446 } else if window_proxy.browsing_context_id() == window_proxy.webview_id() {
447 Ok(JSValue::Window(window.webview_id().to_string()))
448 } else {
449 Ok(JSValue::Frame(
450 window_proxy.browsing_context_id().to_string(),
451 ))
452 }
453 } else if object_has_to_json_property(cx, global_scope, object.handle()) {
454 let name = CString::new("toJSON").unwrap();
455 rooted!(&in(cx) let mut value = UndefinedValue());
456 let call_result = unsafe {
457 JS_CallFunctionName(
458 cx,
459 object.handle(),
460 name.as_ptr(),
461 &HandleValueArray::empty(),
462 value.handle_mut(),
463 )
464 };
465
466 if call_result {
467 Ok(jsval_to_webdriver_inner(
468 cx,
469 global_scope,
470 value.handle(),
471 seen,
472 )?)
473 } else {
474 throw_dom_exception(cx, global_scope, Error::JSFailed);
475 Err(JavaScriptEvaluationError::SerializationError(
476 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
477 ))
478 }
479 } else {
480 clone_an_object(cx, global_scope, val, seen, object.handle())
481 }
482 } else {
483 Err(JavaScriptEvaluationError::SerializationError(
484 JavaScriptEvaluationResultSerializationError::UnknownType,
485 ))
486 }
487}
488
489#[expect(unsafe_code)]
490fn clone_an_object(
492 cx: &mut CurrentRealm,
493 global_scope: &GlobalScope,
494 val: HandleValue,
495 seen: &mut HashSet<HashableJSVal>,
496 object_handle: HandleObject,
497) -> WebDriverJSResult {
498 let hashable = val.into();
499 if seen.contains(&hashable) {
501 return Err(JavaScriptEvaluationError::SerializationError(
502 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
503 ));
504 }
505 seen.insert(hashable.clone());
507
508 let return_val = if is_array_like::<crate::DomTypeHolder>(cx, val) ||
509 is_arguments_object(cx, val)
510 {
511 let mut result: Vec<JSValue> = Vec::new();
512
513 let get_property_result =
514 get_property::<u32>(cx, object_handle, c"length", ConversionBehavior::Default);
515 let length = match get_property_result {
516 Ok(length) => match length {
517 Some(length) => length,
518 _ => {
519 return Err(JavaScriptEvaluationError::SerializationError(
520 JavaScriptEvaluationResultSerializationError::UnknownType,
521 ));
522 },
523 },
524 Err(error) => {
525 throw_dom_exception(cx, global_scope, error);
526 return Err(JavaScriptEvaluationError::SerializationError(
527 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
528 ));
529 },
530 };
531 for i in 0..length {
533 rooted!(&in(cx) let mut item = UndefinedValue());
534 let cname = CString::new(i.to_string()).unwrap();
535 let get_property_result =
536 get_property_jsval(cx, object_handle, &cname, item.handle_mut());
537 match get_property_result {
538 Ok(_) => {
539 let converted_item =
540 jsval_to_webdriver_inner(cx, global_scope, item.handle(), seen)?;
541
542 result.push(converted_item);
543 },
544 Err(error) => {
545 throw_dom_exception(cx, global_scope, error);
546 return Err(JavaScriptEvaluationError::SerializationError(
547 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
548 ));
549 },
550 }
551 }
552 Ok(JSValue::Array(result))
553 } else {
554 let mut result = HashMap::new();
555
556 let mut ids = unsafe { IdVector::new(cx.raw_cx()) };
557 let succeeded =
558 unsafe { GetPropertyKeys(cx, object_handle, JSITER_OWNONLY, ids.handle_mut()) };
559 if !succeeded {
560 return Err(JavaScriptEvaluationError::SerializationError(
561 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
562 ));
563 }
564 for id in ids.iter() {
565 rooted!(&in(cx) let id = *id);
566 rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
567
568 let mut is_none = false;
569 let succeeded = unsafe {
570 JS_GetOwnPropertyDescriptorById(
571 cx,
572 object_handle,
573 id.handle(),
574 desc.handle_mut(),
575 &mut is_none,
576 )
577 };
578 if !succeeded {
579 return Err(JavaScriptEvaluationError::SerializationError(
580 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
581 ));
582 }
583
584 rooted!(&in(cx) let mut property = UndefinedValue());
585 let succeeded = unsafe {
586 JS_GetPropertyById(cx, object_handle, id.handle(), property.handle_mut())
587 };
588 if !succeeded {
589 return Err(JavaScriptEvaluationError::SerializationError(
590 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
591 ));
592 }
593
594 if !property.is_undefined() {
595 let name = jsid_to_string(cx, id.handle());
596 let Some(name) = name else {
597 return Err(JavaScriptEvaluationError::SerializationError(
598 JavaScriptEvaluationResultSerializationError::OtherJavaScriptError,
599 ));
600 };
601
602 let value = jsval_to_webdriver_inner(cx, global_scope, property.handle(), seen)?;
603 result.insert(name.into(), value);
604 }
605 }
606 Ok(JSValue::Object(result))
607 };
608 seen.remove(&hashable);
610 return_val
612}
613
614pub(crate) fn handle_execute_async_script(
615 window: Option<DomRoot<Window>>,
616 eval: String,
617 reply: GenericSender<WebDriverJSResult>,
618 cx: &mut JSContext,
619) {
620 match window {
621 Some(window) => {
622 let reply_sender = reply.clone();
623 window.set_webdriver_script_chan(Some(reply));
624
625 let global_scope = window.as_global_scope();
626
627 let mut realm = enter_auto_realm(cx, global_scope);
628 let mut realm = realm.current_realm();
629 if let Err(error) = global_scope.evaluate_js_on_global(
630 &mut realm,
631 eval.into(),
632 "",
633 None, None,
635 ) {
636 reply_sender.send(Err(error)).unwrap_or_else(|error| {
637 error!("ExecuteAsyncScript Failed to send reply: {error}");
638 });
639 }
640 },
641 None => {
642 reply
643 .send(Err(JavaScriptEvaluationError::DocumentNotFound))
644 .unwrap_or_else(|error| {
645 error!("ExecuteAsyncScript Failed to send reply: {error}");
646 });
647 },
648 }
649}
650
651pub(crate) fn handle_get_parent_frame_id(
653 documents: &DocumentCollection,
654 pipeline: PipelineId,
655 reply: GenericSender<Result<BrowsingContextId, ErrorStatus>>,
656) {
657 reply
660 .send(
661 documents
662 .find_window(pipeline)
663 .and_then(|window| {
664 window
665 .window_proxy()
666 .parent()
667 .map(|parent| parent.browsing_context_id())
668 })
669 .ok_or(ErrorStatus::NoSuchWindow),
670 )
671 .unwrap();
672}
673
674pub(crate) fn handle_get_browsing_context_id(
676 documents: &DocumentCollection,
677 pipeline: PipelineId,
678 webdriver_frame_id: WebDriverFrameId,
679 reply: GenericSender<Result<BrowsingContextId, ErrorStatus>>,
680) {
681 reply
682 .send(match webdriver_frame_id {
683 WebDriverFrameId::Short(id) => {
684 documents
687 .find_document(pipeline)
688 .ok_or(ErrorStatus::NoSuchWindow)
689 .and_then(|document| {
690 document
691 .iframes()
692 .iter()
693 .nth(id as usize)
694 .and_then(|iframe| iframe.browsing_context_id())
695 .ok_or(ErrorStatus::NoSuchFrame)
696 })
697 },
698 WebDriverFrameId::Element(element_id) => {
699 get_known_element(documents, pipeline, element_id).and_then(|element| {
700 element
701 .downcast::<HTMLIFrameElement>()
702 .and_then(|element| element.browsing_context_id())
703 .ok_or(ErrorStatus::NoSuchFrame)
704 })
705 },
706 })
707 .unwrap();
708}
709
710fn get_element_in_view_center_point(cx: &mut JSContext, element: &Element) -> Option<Point2D<i64>> {
712 let doc = element.owner_document();
713 element.GetClientRects(cx).first().map(|rectangle| {
716 let x = rectangle.X();
717 let y = rectangle.Y();
718 let width = rectangle.Width();
719 let height = rectangle.Height();
720 debug!(
721 "get_element_in_view_center_point: Element rectangle at \
722 (x: {x}, y: {y}, width: {width}, height: {height})",
723 );
724 let window = doc.window();
725 let left = (x.min(x + width)).max(0.0);
727 let right = f64::min(window.InnerWidth() as f64, x.max(x + width));
729 let top = (y.min(y + height)).max(0.0);
731 let bottom = f64::min(window.InnerHeight() as f64, y.max(y + height));
734 debug!(
735 "get_element_in_view_center_point: Computed rectangle is \
736 (left: {left}, right: {right}, top: {top}, bottom: {bottom})",
737 );
738 let center_x = ((left + right) / 2.0).floor() as i64;
740 let center_y = ((top + bottom) / 2.0).floor() as i64;
742
743 debug!(
744 "get_element_in_view_center_point: Element center point at ({center_x}, {center_y})",
745 );
746 Point2D::new(center_x, center_y)
748 })
749}
750
751pub(crate) fn handle_get_element_in_view_center_point(
752 cx: &mut JSContext,
753 documents: &DocumentCollection,
754 pipeline: PipelineId,
755 element_id: String,
756 reply: GenericOneshotSender<Result<Option<(i64, i64)>, ErrorStatus>>,
757) {
758 reply
759 .send(
760 get_known_element(documents, pipeline, element_id).map(|element| {
761 get_element_in_view_center_point(cx, &element).map(|point| (point.x, point.y))
762 }),
763 )
764 .unwrap();
765}
766
767fn retrieve_document_and_check_root_existence(
768 documents: &DocumentCollection,
769 pipeline: PipelineId,
770) -> Result<DomRoot<Document>, ErrorStatus> {
771 let document = documents
772 .find_document(pipeline)
773 .ok_or(ErrorStatus::NoSuchWindow)?;
774
775 if document.GetDocumentElement().is_none() {
780 Err(ErrorStatus::NoSuchElement)
781 } else {
782 Ok(document)
783 }
784}
785
786pub(crate) fn handle_find_elements_css_selector(
787 cx: &mut JSContext,
788 documents: &DocumentCollection,
789 pipeline: PipelineId,
790 selector: String,
791 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
792) {
793 match retrieve_document_and_check_root_existence(documents, pipeline) {
794 Ok(document) => reply
795 .send(
796 document
797 .QuerySelectorAll(cx, DOMString::from(selector))
798 .map_err(|_| ErrorStatus::InvalidSelector)
799 .map(|nodes| {
800 nodes
801 .iter(cx)
802 .map(|x| x.upcast::<Node>().unique_id(pipeline))
803 .collect()
804 }),
805 )
806 .unwrap(),
807 Err(error) => reply.send(Err(error)).unwrap(),
808 }
809}
810
811pub(crate) fn handle_find_elements_link_text(
812 cx: &mut JSContext,
813 documents: &DocumentCollection,
814 pipeline: PipelineId,
815 selector: String,
816 partial: bool,
817 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
818) {
819 match retrieve_document_and_check_root_existence(documents, pipeline) {
820 Ok(document) => reply
821 .send(all_matching_links(
822 cx,
823 document.upcast::<Node>(),
824 selector,
825 partial,
826 ))
827 .unwrap(),
828 Err(error) => reply.send(Err(error)).unwrap(),
829 }
830}
831
832pub(crate) fn handle_find_elements_tag_name(
833 cx: &mut JSContext,
834 documents: &DocumentCollection,
835 pipeline: PipelineId,
836 selector: String,
837 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
838) {
839 match retrieve_document_and_check_root_existence(documents, pipeline) {
840 Ok(document) => reply
841 .send(Ok(document
842 .GetElementsByTagName(cx, DOMString::from(selector))
843 .elements_iter(cx.no_gc())
844 .map(|x| x.upcast::<Node>().unique_id(pipeline))
845 .collect::<Vec<String>>()))
846 .unwrap(),
847 Err(error) => reply.send(Err(error)).unwrap(),
848 }
849}
850
851fn find_elements_xpath_strategy(
853 cx: &mut JSContext,
854 document: &Document,
855 start_node: &Node,
856 selector: String,
857 pipeline: PipelineId,
858) -> Result<Vec<String>, ErrorStatus> {
859 let evaluate_result = match document.Evaluate(
864 cx,
865 DOMString::from(selector),
866 start_node,
867 None,
868 XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE,
869 None,
870 ) {
871 Ok(res) => res,
872 Err(_) => return Err(ErrorStatus::InvalidSelector),
873 };
874 let length = match evaluate_result.GetSnapshotLength() {
880 Ok(len) => len,
881 Err(_) => return Err(ErrorStatus::InvalidSelector),
882 };
883
884 let mut result = Vec::new();
886
887 for index in 0..length {
889 let node = match evaluate_result.SnapshotItem(index) {
892 Ok(node) => node.expect(
893 "Node should always exist as ORDERED_NODE_SNAPSHOT_TYPE \
894 gives static result and we verified the length!",
895 ),
896 Err(_) => return Err(ErrorStatus::InvalidSelector),
897 };
898
899 if !node.is::<Element>() {
901 return Err(ErrorStatus::InvalidSelector);
902 }
903
904 result.push(node.unique_id(pipeline));
906 }
907 Ok(result)
909}
910
911pub(crate) fn handle_find_elements_xpath_selector(
912 cx: &mut JSContext,
913 documents: &DocumentCollection,
914 pipeline: PipelineId,
915 selector: String,
916 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
917) {
918 match retrieve_document_and_check_root_existence(documents, pipeline) {
919 Ok(document) => reply
920 .send(find_elements_xpath_strategy(
921 cx,
922 &document,
923 document.upcast::<Node>(),
924 selector,
925 pipeline,
926 ))
927 .unwrap(),
928 Err(error) => reply.send(Err(error)).unwrap(),
929 }
930}
931
932pub(crate) fn handle_find_element_elements_css_selector(
933 cx: &mut JSContext,
934 documents: &DocumentCollection,
935 pipeline: PipelineId,
936 element_id: String,
937 selector: String,
938 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
939) {
940 reply
941 .send(
942 get_known_element(documents, pipeline, element_id).and_then(|element| {
943 element
944 .upcast::<Node>()
945 .query_selector_all(cx, DOMString::from(selector))
946 .map_err(|_| ErrorStatus::InvalidSelector)
947 .map(|nodes| {
948 nodes
949 .iter(cx)
950 .map(|x| x.upcast::<Node>().unique_id(pipeline))
951 .collect()
952 })
953 }),
954 )
955 .unwrap();
956}
957
958pub(crate) fn handle_find_element_elements_link_text(
959 cx: &mut JSContext,
960 documents: &DocumentCollection,
961 pipeline: PipelineId,
962 element_id: String,
963 selector: String,
964 partial: bool,
965 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
966) {
967 reply
968 .send(
969 get_known_element(documents, pipeline, element_id).and_then(|element| {
970 all_matching_links(cx, element.upcast::<Node>(), selector.clone(), partial)
971 }),
972 )
973 .unwrap();
974}
975
976pub(crate) fn handle_find_element_elements_tag_name(
977 cx: &mut JSContext,
978 documents: &DocumentCollection,
979 pipeline: PipelineId,
980 element_id: String,
981 selector: String,
982 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
983) {
984 reply
985 .send(
986 get_known_element(documents, pipeline, element_id).map(|element| {
987 element
988 .GetElementsByTagName(cx, DOMString::from(selector))
989 .elements_iter(cx.no_gc())
990 .map(|x| x.upcast::<Node>().unique_id(pipeline))
991 .collect::<Vec<String>>()
992 }),
993 )
994 .unwrap();
995}
996
997pub(crate) fn handle_find_element_elements_xpath_selector(
998 cx: &mut JSContext,
999 documents: &DocumentCollection,
1000 pipeline: PipelineId,
1001 element_id: String,
1002 selector: String,
1003 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1004) {
1005 reply
1006 .send(
1007 get_known_element(documents, pipeline, element_id).and_then(|element| {
1008 find_elements_xpath_strategy(
1009 cx,
1010 &documents
1011 .find_document(pipeline)
1012 .expect("Document existence guaranteed by `get_known_element`"),
1013 element.upcast::<Node>(),
1014 selector,
1015 pipeline,
1016 )
1017 }),
1018 )
1019 .unwrap();
1020}
1021
1022pub(crate) fn handle_find_shadow_elements_css_selector(
1024 cx: &mut JSContext,
1025 documents: &DocumentCollection,
1026 pipeline: PipelineId,
1027 shadow_root_id: String,
1028 selector: String,
1029 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1030) {
1031 reply
1032 .send(
1033 get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1034 shadow_root
1035 .upcast::<Node>()
1036 .query_selector_all(cx, DOMString::from(selector))
1037 .map_err(|_| ErrorStatus::InvalidSelector)
1038 .map(|nodes| {
1039 nodes
1040 .iter(cx)
1041 .map(|x| x.upcast::<Node>().unique_id(pipeline))
1042 .collect()
1043 })
1044 }),
1045 )
1046 .unwrap();
1047}
1048
1049pub(crate) fn handle_find_shadow_elements_link_text(
1050 cx: &mut JSContext,
1051 documents: &DocumentCollection,
1052 pipeline: PipelineId,
1053 shadow_root_id: String,
1054 selector: String,
1055 partial: bool,
1056 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1057) {
1058 reply
1059 .send(
1060 get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1061 all_matching_links(cx, shadow_root.upcast::<Node>(), selector.clone(), partial)
1062 }),
1063 )
1064 .unwrap();
1065}
1066
1067pub(crate) fn handle_find_shadow_elements_tag_name(
1068 cx: &mut JSContext,
1069 documents: &DocumentCollection,
1070 pipeline: PipelineId,
1071 shadow_root_id: String,
1072 selector: String,
1073 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1074) {
1075 reply
1081 .send(
1082 get_known_shadow_root(documents, pipeline, shadow_root_id).map(|shadow_root| {
1083 shadow_root
1084 .upcast::<Node>()
1085 .query_selector_all(cx, DOMString::from(selector))
1086 .map(|nodes| {
1087 nodes
1088 .iter(cx)
1089 .map(|x| x.upcast::<Node>().unique_id(pipeline))
1090 .collect()
1091 })
1092 .unwrap_or_default()
1093 }),
1094 )
1095 .unwrap();
1096}
1097
1098pub(crate) fn handle_find_shadow_elements_xpath_selector(
1099 cx: &mut JSContext,
1100 documents: &DocumentCollection,
1101 pipeline: PipelineId,
1102 shadow_root_id: String,
1103 selector: String,
1104 reply: GenericSender<Result<Vec<String>, ErrorStatus>>,
1105) {
1106 reply
1107 .send(
1108 get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
1109 find_elements_xpath_strategy(
1110 cx,
1111 &documents
1112 .find_document(pipeline)
1113 .expect("Document existence guaranteed by `get_known_shadow_root`"),
1114 shadow_root.upcast::<Node>(),
1115 selector,
1116 pipeline,
1117 )
1118 }),
1119 )
1120 .unwrap();
1121}
1122
1123pub(crate) fn handle_get_element_shadow_root(
1125 documents: &DocumentCollection,
1126 pipeline: PipelineId,
1127 element_id: String,
1128 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1129) {
1130 reply
1131 .send(
1132 get_known_element(documents, pipeline, element_id).map(|element| {
1133 element
1134 .shadow_root()
1135 .map(|x| x.upcast::<Node>().unique_id(pipeline))
1136 }),
1137 )
1138 .unwrap();
1139}
1140
1141impl Element {
1142 fn is_keyboard_interactable(&self, no_gc: &NoGC) -> bool {
1144 self.is_focusable_area(no_gc) || self.is::<HTMLBodyElement>() || self.is_document_element()
1145 }
1146}
1147
1148fn handle_send_keys_file(
1149 file_input: &HTMLInputElement,
1150 text: &str,
1151 reply_sender: GenericSender<Result<bool, ErrorStatus>>,
1152) {
1153 let files: Vec<DOMString> = text
1158 .split("\n")
1159 .filter_map(|string| {
1160 if string.is_empty() {
1161 None
1162 } else {
1163 Some(string.into())
1164 }
1165 })
1166 .collect();
1167
1168 if files.is_empty() {
1170 let _ = reply_sender.send(Err(ErrorStatus::InvalidArgument));
1171 return;
1172 }
1173
1174 if !file_input.Multiple() && files.len() > 1 {
1178 let _ = reply_sender.send(Err(ErrorStatus::InvalidArgument));
1179 return;
1180 }
1181
1182 file_input.select_files_for_webdriver(files, reply_sender);
1191}
1192
1193fn handle_send_keys_non_typeable(
1195 cx: &mut JSContext,
1196 input_element: &HTMLInputElement,
1197 text: &str,
1198) -> Result<bool, ErrorStatus> {
1199 if !input_element.is_mutable() {
1206 return Err(ErrorStatus::ElementNotInteractable);
1207 }
1208
1209 if let Err(error) = input_element.SetValue(cx, text.into()) {
1211 error!(
1212 "Failed to set value on non-typeable input element: {:?}",
1213 error
1214 );
1215 return Err(ErrorStatus::UnknownError);
1216 }
1217
1218 if input_element
1220 .Validity(cx)
1221 .invalid_flags()
1222 .contains(ValidationFlags::BAD_INPUT)
1223 {
1224 return Err(ErrorStatus::InvalidArgument);
1225 }
1226
1227 Ok(false)
1230}
1231
1232pub(crate) fn handle_will_send_keys(
1238 cx: &mut JSContext,
1239 documents: &DocumentCollection,
1240 pipeline: PipelineId,
1241 element_id: String,
1242 text: String,
1243 strict_file_interactability: bool,
1244 reply: GenericSender<Result<bool, ErrorStatus>>,
1245) {
1246 let element = match get_known_element(documents, pipeline, element_id) {
1248 Ok(element) => element,
1249 Err(error) => {
1250 let _ = reply.send(Err(error));
1251 return;
1252 },
1253 };
1254
1255 let input_element = element.downcast::<HTMLInputElement>();
1256 let mut element_has_focus = false;
1257
1258 let is_file_input =
1261 input_element.is_some_and(|e| matches!(*e.input_type(), InputType::File(_)));
1262
1263 if !is_file_input || strict_file_interactability {
1265 scroll_into_view(cx, &element);
1267
1268 if !element.is_keyboard_interactable(cx.no_gc()) {
1274 let _ = reply.send(Err(ErrorStatus::ElementNotInteractable));
1275 return;
1276 }
1277
1278 let Some(html_element) = element.downcast::<HTMLElement>() else {
1281 let _ = reply.send(Err(ErrorStatus::UnknownError));
1282 return;
1283 };
1284
1285 if !element.is_active_element() {
1286 html_element.Focus(
1287 cx,
1288 &FocusOptions {
1289 preventScroll: true,
1290 },
1291 );
1292 } else {
1293 element_has_focus = element.focus_state();
1294 }
1295 }
1296
1297 if let Some(input_element) = input_element {
1298 if is_file_input {
1300 handle_send_keys_file(input_element, &text, reply);
1301 return;
1302 }
1303
1304 if input_element.is_nontypeable() {
1306 let _ = reply.send(handle_send_keys_non_typeable(cx, input_element, &text));
1307 return;
1308 }
1309 }
1310
1311 if !element_has_focus {
1319 if let Some(input_element) = input_element {
1320 let length = input_element.Value().len() as u32;
1321 let _ = input_element.SetSelectionRange(length, length, None);
1322 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1323 let length = textarea_element.Value().len() as u32;
1324 let _ = textarea_element.SetSelectionRange(length, length, None);
1325 }
1326 }
1327
1328 let _ = reply.send(Ok(true));
1329}
1330
1331pub(crate) fn handle_get_active_element(
1332 documents: &DocumentCollection,
1333 pipeline: PipelineId,
1334 reply: GenericSender<Option<String>>,
1335) {
1336 reply
1337 .send(
1338 documents
1339 .find_document(pipeline)
1340 .and_then(|document| document.GetActiveElement())
1341 .map(|element| element.upcast::<Node>().unique_id(pipeline)),
1342 )
1343 .unwrap();
1344}
1345
1346pub(crate) fn handle_get_computed_role(
1347 documents: &DocumentCollection,
1348 pipeline: PipelineId,
1349 node_id: String,
1350 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1351) {
1352 reply
1353 .send(
1354 get_known_element(documents, pipeline, node_id)
1355 .map(|element| element.GetRole().map(String::from)),
1359 )
1360 .unwrap();
1361}
1362
1363pub(crate) fn handle_get_page_source(
1364 cx: &mut JSContext,
1365 documents: &DocumentCollection,
1366 pipeline: PipelineId,
1367 reply: GenericSender<Result<String, ErrorStatus>>,
1368) {
1369 reply
1370 .send(
1371 documents
1372 .find_document(pipeline)
1373 .ok_or(ErrorStatus::UnknownError)
1374 .and_then(|document| match document.GetDocumentElement() {
1375 Some(element) => match element.outer_html(cx) {
1376 Ok(source) => Ok(String::from(source)),
1377 Err(_) => {
1378 match XMLSerializer::new(cx, document.window(), None)
1379 .SerializeToString(element.upcast::<Node>())
1380 {
1381 Ok(source) => Ok(String::from(source)),
1382 Err(_) => Err(ErrorStatus::UnknownError),
1383 }
1384 },
1385 },
1386 None => Err(ErrorStatus::UnknownError),
1387 }),
1388 )
1389 .unwrap();
1390}
1391
1392pub(crate) fn handle_get_cookies(
1393 documents: &DocumentCollection,
1394 pipeline: PipelineId,
1395 reply: GenericSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
1396) {
1397 reply
1398 .send(
1399 match documents.find_document(pipeline) {
1401 Some(document) => {
1402 let url = document.url();
1403 let (sender, receiver) = generic_channel::channel().unwrap();
1404 let _ = document
1405 .window()
1406 .as_global_scope()
1407 .resource_threads()
1408 .send(GetCookiesForUrl(url, sender, NonHTTP));
1409 Ok(receiver.recv().unwrap())
1410 },
1411 None => Ok(Vec::new()),
1412 },
1413 )
1414 .unwrap();
1415}
1416
1417pub(crate) fn handle_get_cookie(
1419 documents: &DocumentCollection,
1420 pipeline: PipelineId,
1421 name: String,
1422 reply: GenericSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
1423) {
1424 reply
1425 .send(
1426 match documents.find_document(pipeline) {
1428 Some(document) => {
1429 let url = document.url();
1430 let (sender, receiver) = generic_channel::channel().unwrap();
1431 let _ = document
1432 .window()
1433 .as_global_scope()
1434 .resource_threads()
1435 .send(GetCookiesForUrl(url, sender, NonHTTP));
1436 let cookies = receiver.recv().unwrap();
1437 Ok(cookies
1438 .into_iter()
1439 .filter(|cookie| cookie.name() == &*name)
1440 .collect())
1441 },
1442 None => Ok(Vec::new()),
1443 },
1444 )
1445 .unwrap();
1446}
1447
1448pub(crate) fn handle_add_cookie(
1450 documents: &DocumentCollection,
1451 pipeline: PipelineId,
1452 cookie: Cookie<'static>,
1453 reply: GenericSender<Result<(), ErrorStatus>>,
1454) {
1455 let document = match documents.find_document(pipeline) {
1457 Some(document) => document,
1458 None => {
1459 return reply.send(Err(ErrorStatus::NoSuchWindow)).unwrap();
1460 },
1461 };
1462 let url = document.url();
1463 let method = if cookie.http_only().unwrap_or(false) {
1464 HTTP
1465 } else {
1466 NonHTTP
1467 };
1468
1469 let domain = cookie.domain().map(ToOwned::to_owned);
1470 reply
1472 .send(match (document.is_cookie_averse(), domain) {
1473 (true, _) => Err(ErrorStatus::InvalidCookieDomain),
1476 (false, Some(ref domain)) if url.host_str().is_some_and(|host| host == domain) => {
1477 let _ = document
1478 .window()
1479 .as_global_scope()
1480 .resource_threads()
1481 .send(SetCookieForUrl(url, Serde(cookie), method, None));
1482 Ok(())
1483 },
1484 (false, Some(_)) => Err(ErrorStatus::InvalidCookieDomain),
1487 (false, None) => {
1488 let _ = document
1489 .window()
1490 .as_global_scope()
1491 .resource_threads()
1492 .send(SetCookieForUrl(url, Serde(cookie), method, None));
1493 Ok(())
1494 },
1495 })
1496 .unwrap();
1497}
1498
1499pub(crate) fn handle_delete_cookies(
1501 documents: &DocumentCollection,
1502 pipeline: PipelineId,
1503 reply: GenericSender<Result<(), ErrorStatus>>,
1504) {
1505 let document = match documents.find_document(pipeline) {
1506 Some(document) => document,
1507 None => {
1508 return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
1509 },
1510 };
1511 let url = document.url();
1512 document
1513 .window()
1514 .as_global_scope()
1515 .resource_threads()
1516 .send(DeleteCookies(Some(url), None))
1517 .unwrap();
1518 reply.send(Ok(())).unwrap();
1519}
1520
1521pub(crate) fn handle_delete_cookie(
1523 documents: &DocumentCollection,
1524 pipeline: PipelineId,
1525 name: String,
1526 reply: GenericSender<Result<(), ErrorStatus>>,
1527) {
1528 let document = match documents.find_document(pipeline) {
1529 Some(document) => document,
1530 None => {
1531 return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
1532 },
1533 };
1534 let url = document.url();
1535 document
1536 .window()
1537 .as_global_scope()
1538 .resource_threads()
1539 .send(DeleteCookie(url, name))
1540 .unwrap();
1541 reply.send(Ok(())).unwrap();
1542}
1543
1544pub(crate) fn handle_get_title(
1545 documents: &DocumentCollection,
1546 pipeline: PipelineId,
1547 reply: GenericSender<String>,
1548) {
1549 reply
1550 .send(
1551 documents
1553 .find_document(pipeline)
1554 .map(|document| String::from(document.Title()))
1555 .unwrap_or_default(),
1556 )
1557 .unwrap();
1558}
1559
1560fn calculate_absolute_position(
1562 documents: &DocumentCollection,
1563 pipeline: &PipelineId,
1564 rect: &DOMRect,
1565) -> Result<(f64, f64), ErrorStatus> {
1566 let document = match documents.find_document(*pipeline) {
1571 Some(document) => document,
1572 None => return Err(ErrorStatus::UnknownError),
1573 };
1574 let win = match document.GetDefaultView() {
1575 Some(win) => win,
1576 None => return Err(ErrorStatus::UnknownError),
1577 };
1578
1579 let x = win.ScrollX() as f64 + rect.X();
1581 let y = win.ScrollY() as f64 + rect.Y();
1582
1583 Ok((x, y))
1584}
1585
1586pub(crate) fn handle_get_rect(
1588 cx: &mut JSContext,
1589 documents: &DocumentCollection,
1590 pipeline: PipelineId,
1591 element_id: String,
1592 reply: GenericSender<Result<Rect<f64>, ErrorStatus>>,
1593) {
1594 reply
1595 .send(
1596 get_known_element(documents, pipeline, element_id).and_then(|element| {
1597 let rect = element.GetBoundingClientRect(cx);
1601 let (x, y) = calculate_absolute_position(documents, &pipeline, &rect)?;
1602
1603 Ok(Rect::new(
1605 Point2D::new(x, y),
1606 Size2D::new(rect.Width(), rect.Height()),
1607 ))
1608 }),
1609 )
1610 .unwrap();
1611}
1612
1613pub(crate) fn handle_scroll_and_get_bounding_client_rect(
1614 cx: &mut JSContext,
1615 documents: &DocumentCollection,
1616 pipeline: PipelineId,
1617 element_id: String,
1618 reply: GenericSender<Result<Rect<f32>, ErrorStatus>>,
1619) {
1620 reply
1621 .send(
1622 get_known_element(documents, pipeline, element_id).map(|element| {
1623 scroll_into_view(cx, &element);
1624
1625 let rect = element.GetBoundingClientRect(cx);
1626 Rect::new(
1627 Point2D::new(rect.X() as f32, rect.Y() as f32),
1628 Size2D::new(rect.Width() as f32, rect.Height() as f32),
1629 )
1630 }),
1631 )
1632 .unwrap();
1633}
1634
1635pub(crate) fn handle_get_text(
1637 documents: &DocumentCollection,
1638 pipeline: PipelineId,
1639 node_id: String,
1640 reply: GenericSender<Result<String, ErrorStatus>>,
1641) {
1642 reply
1643 .send(
1644 get_known_element(documents, pipeline, node_id).map(|element| {
1645 element
1646 .downcast::<HTMLElement>()
1647 .map(|htmlelement| String::from(htmlelement.InnerText()))
1648 .unwrap_or_else(|| {
1649 element
1650 .upcast::<Node>()
1651 .GetTextContent()
1652 .map_or("".to_owned(), String::from)
1653 })
1654 }),
1655 )
1656 .unwrap();
1657}
1658
1659pub(crate) fn handle_get_name(
1660 documents: &DocumentCollection,
1661 pipeline: PipelineId,
1662 node_id: String,
1663 reply: GenericSender<Result<String, ErrorStatus>>,
1664) {
1665 reply
1666 .send(
1667 get_known_element(documents, pipeline, node_id)
1668 .map(|element| String::from(element.TagName())),
1669 )
1670 .unwrap();
1671}
1672
1673pub(crate) fn handle_get_attribute(
1674 cx: &mut JSContext,
1675 documents: &DocumentCollection,
1676 pipeline: PipelineId,
1677 node_id: String,
1678 name: String,
1679 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1680) {
1681 reply
1682 .send(
1683 get_known_element(documents, pipeline, node_id).map(|element| {
1684 if is_boolean_attribute(&name) {
1685 if element.HasAttribute(cx, DOMString::from(name)) {
1686 Some(String::from("true"))
1687 } else {
1688 None
1689 }
1690 } else {
1691 element
1692 .GetAttribute(cx, DOMString::from(name))
1693 .map(String::from)
1694 }
1695 }),
1696 )
1697 .unwrap();
1698}
1699
1700pub(crate) fn handle_get_property(
1701 documents: &DocumentCollection,
1702 pipeline: PipelineId,
1703 node_id: String,
1704 name: String,
1705 reply: GenericSender<Result<JSValue, ErrorStatus>>,
1706 cx: &mut JSContext,
1707) {
1708 reply
1709 .send(
1710 get_known_element(documents, pipeline, node_id).map(|element| {
1711 let document = documents.find_document(pipeline).unwrap();
1712
1713 let Ok(cname) = CString::new(name) else {
1714 return JSValue::Undefined;
1715 };
1716
1717 let mut realm = enter_auto_realm(cx, &*document);
1718 let cx = &mut realm.current_realm();
1719
1720 rooted!(&in(cx) let mut property = UndefinedValue());
1721 match get_property_jsval(
1722 cx,
1723 element.reflector().get_jsobject(),
1724 &cname,
1725 property.handle_mut(),
1726 ) {
1727 Ok(_) => match jsval_to_webdriver(cx, &element.global(), property.handle()) {
1728 Ok(property) => property,
1729 Err(_) => JSValue::Undefined,
1730 },
1731 Err(error) => {
1732 throw_dom_exception(cx, &element.global(), error);
1733 JSValue::Undefined
1734 },
1735 }
1736 }),
1737 )
1738 .unwrap();
1739}
1740
1741pub(crate) fn handle_get_css(
1742 cx: &mut JSContext,
1743 documents: &DocumentCollection,
1744 pipeline: PipelineId,
1745 node_id: String,
1746 name: String,
1747 reply: GenericSender<Result<String, ErrorStatus>>,
1748) {
1749 reply
1750 .send(
1751 get_known_element(documents, pipeline, node_id).map(|element| {
1752 let window = element.owner_window();
1753 String::from(
1754 window
1755 .GetComputedStyle(cx, &element, None)
1756 .GetPropertyValue(DOMString::from(name)),
1757 )
1758 }),
1759 )
1760 .unwrap();
1761}
1762
1763pub(crate) fn handle_get_url(
1764 documents: &DocumentCollection,
1765 pipeline: PipelineId,
1766 reply: GenericSender<String>,
1767) {
1768 reply
1769 .send(
1770 documents
1772 .find_document(pipeline)
1773 .map(|document| document.url().into_string())
1774 .unwrap_or_else(|| "about:blank".to_string()),
1775 )
1776 .unwrap();
1777}
1778
1779fn element_is_mutable_form_control(element: &Element) -> bool {
1781 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1782 input_element.is_mutable() &&
1783 matches!(
1784 *input_element.input_type(),
1785 InputType::Text(_) |
1786 InputType::Search(_) |
1787 InputType::Url(_) |
1788 InputType::Tel(_) |
1789 InputType::Email(_) |
1790 InputType::Password(_) |
1791 InputType::Date(_) |
1792 InputType::Month(_) |
1793 InputType::Week(_) |
1794 InputType::Time(_) |
1795 InputType::DatetimeLocal(_) |
1796 InputType::Number(_) |
1797 InputType::Range(_) |
1798 InputType::Color(_) |
1799 InputType::File(_)
1800 )
1801 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1802 textarea_element.is_mutable()
1803 } else {
1804 false
1805 }
1806}
1807
1808fn clear_a_resettable_element(cx: &mut JSContext, element: &Element) -> Result<(), ErrorStatus> {
1810 let html_element = element
1811 .downcast::<HTMLElement>()
1812 .ok_or(ErrorStatus::UnknownError)?;
1813
1814 if html_element.is_candidate_for_constraint_validation() {
1817 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1818 if input_element.Value().is_empty() {
1819 return Ok(());
1820 }
1821 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() &&
1822 textarea_element.Value().is_empty()
1823 {
1824 return Ok(());
1825 }
1826 }
1827
1828 html_element.Focus(
1830 cx,
1831 &FocusOptions {
1832 preventScroll: true,
1833 },
1834 );
1835
1836 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
1838 input_element.clear(cx);
1839 } else if let Some(textarea_element) = element.downcast::<HTMLTextAreaElement>() {
1840 textarea_element.clear();
1841 } else {
1842 unreachable!("We have confirm previously that element is mutable form control");
1843 }
1844
1845 let event_target = element.upcast::<EventTarget>();
1846 event_target.fire_bubbling_event(cx, atom!("input"));
1847 event_target.fire_bubbling_event(cx, atom!("change"));
1848
1849 html_element.Blur(cx);
1851
1852 Ok(())
1853}
1854
1855pub(crate) fn handle_element_clear(
1857 cx: &mut JSContext,
1858 documents: &DocumentCollection,
1859 pipeline: PipelineId,
1860 element_id: String,
1861 reply: GenericSender<Result<(), ErrorStatus>>,
1862) {
1863 reply
1864 .send(
1865 get_known_element(documents, pipeline, element_id).and_then(|element| {
1866 if !element_is_mutable_form_control(&element) {
1870 return Err(ErrorStatus::InvalidElementState);
1871 }
1872
1873 scroll_into_view(cx, &element);
1875
1876 if !element.is_keyboard_interactable(cx.no_gc()) {
1882 return Err(ErrorStatus::ElementNotInteractable);
1883 }
1884
1885 let paint_tree = get_element_pointer_interactable_paint_tree(cx, &element);
1886 if !is_element_in_view(&element, &paint_tree) {
1887 return Err(ErrorStatus::ElementNotInteractable);
1888 }
1889
1890 clear_a_resettable_element(cx, &element)
1893 }),
1894 )
1895 .unwrap();
1896}
1897
1898fn get_option_parent(node: &Node) -> Option<DomRoot<Element>> {
1899 let mut candidate_select = None;
1907
1908 for ancestor in node.ancestors() {
1909 if ancestor.is::<HTMLDataListElement>() {
1910 return Some(DomRoot::downcast::<Element>(ancestor).unwrap());
1911 } else if candidate_select.is_none() && ancestor.is::<HTMLSelectElement>() {
1912 candidate_select = Some(ancestor);
1913 }
1914 }
1915
1916 candidate_select.map(|ancestor| DomRoot::downcast::<Element>(ancestor).unwrap())
1917}
1918
1919fn get_container(element: &Element) -> Option<DomRoot<Element>> {
1921 if element.is::<HTMLOptionElement>() {
1922 return get_option_parent(element.upcast::<Node>());
1923 }
1924 if element.is::<HTMLOptGroupElement>() {
1925 return get_option_parent(element.upcast::<Node>())
1926 .or_else(|| Some(DomRoot::from_ref(element)));
1927 }
1928 Some(DomRoot::from_ref(element))
1929}
1930
1931pub(crate) fn handle_element_click(
1933 cx: &mut JSContext,
1934 documents: &DocumentCollection,
1935 pipeline: PipelineId,
1936 element_id: String,
1937 reply: GenericSender<Result<Option<String>, ErrorStatus>>,
1938) {
1939 reply
1940 .send(
1941 get_known_element(documents, pipeline, element_id).and_then(|element| {
1943 if let Some(input_element) = element.downcast::<HTMLInputElement>() &&
1946 matches!(*input_element.input_type(), InputType::File(_))
1947 {
1948 return Err(ErrorStatus::InvalidArgument);
1949 }
1950
1951 let Some(container) = get_container(&element) else {
1952 return Err(ErrorStatus::UnknownError);
1953 };
1954
1955 scroll_into_view(cx, &container);
1957
1958 let paint_tree = get_element_pointer_interactable_paint_tree(cx, &container);
1961
1962 if !is_element_in_view(&container, &paint_tree) {
1963 return Err(ErrorStatus::ElementNotInteractable);
1964 }
1965
1966 if !container
1973 .upcast::<Node>()
1974 .is_shadow_including_inclusive_ancestor_of(paint_tree[0].upcast::<Node>())
1975 {
1976 return Err(ErrorStatus::ElementClickIntercepted);
1977 }
1978
1979 match element.downcast::<HTMLOptionElement>() {
1981 Some(option_element) => {
1982 let event_target = container.upcast::<EventTarget>();
1984 event_target.fire_event(cx, atom!("mouseover"));
1985 event_target.fire_event(cx, atom!("mousemove"));
1986 event_target.fire_event(cx, atom!("mousedown"));
1987
1988 match container.downcast::<HTMLElement>() {
1990 Some(html_element) => {
1991 html_element.Focus(
1992 cx,
1993 &FocusOptions {
1994 preventScroll: true,
1995 },
1996 );
1997 },
1998 None => return Err(ErrorStatus::UnknownError),
1999 }
2000
2001 if !is_disabled(&element) {
2003 event_target.fire_event(cx, atom!("input"));
2005
2006 let previous_selectedness = option_element.Selected();
2008
2009 match container.downcast::<HTMLSelectElement>() {
2011 Some(select_element) => {
2012 if select_element.Multiple() {
2013 option_element.SetSelected(cx, !option_element.Selected());
2014 }
2015 },
2016 None => option_element.SetSelected(cx, true),
2017 }
2018
2019 if !previous_selectedness {
2021 event_target.fire_event(cx, atom!("change"));
2022 }
2023 }
2024
2025 event_target.fire_event(cx, atom!("mouseup"));
2027 event_target.fire_event(cx, atom!("click"));
2028
2029 Ok(None)
2030 },
2031 None => Ok(Some(element.upcast::<Node>().unique_id(pipeline))),
2032 }
2033 }),
2034 )
2035 .unwrap();
2036}
2037
2038fn is_element_in_view(element: &Element, paint_tree: &[DomRoot<Element>]) -> bool {
2040 if !paint_tree.contains(&DomRoot::from_ref(element)) {
2043 return false;
2044 }
2045 use style::computed_values::pointer_events::T as PointerEvents;
2046 element
2050 .style()
2051 .is_none_or(|style| style.get_inherited_ui().pointer_events != PointerEvents::None)
2052}
2053
2054fn get_element_pointer_interactable_paint_tree(
2056 cx: &mut JSContext,
2057 element: &Element,
2058) -> Vec<DomRoot<Element>> {
2059 if !element.is_connected() {
2062 return Vec::new();
2063 }
2064
2065 get_element_in_view_center_point(cx, element).map_or(Vec::new(), |center_point| {
2071 if let Some(shadow_root) = element.containing_shadow_root() {
2072 shadow_root.ElementsFromPoint(
2073 Finite::wrap(center_point.x as f64),
2074 Finite::wrap(center_point.y as f64),
2075 )
2076 } else {
2077 element.owner_document().ElementsFromPoint(
2078 Finite::wrap(center_point.x as f64),
2079 Finite::wrap(center_point.y as f64),
2080 )
2081 }
2082 })
2083}
2084
2085pub(crate) fn handle_is_enabled(
2087 documents: &DocumentCollection,
2088 pipeline: PipelineId,
2089 element_id: String,
2090 reply: GenericSender<Result<bool, ErrorStatus>>,
2091) {
2092 reply
2093 .send(
2094 get_known_element(documents, pipeline, element_id).map(|element| {
2096 let document = documents.find_document(pipeline).unwrap();
2098
2099 if document.is_html_document() || document.is_xhtml_document() {
2105 !is_disabled(&element)
2106 } else {
2107 false
2108 }
2109 }),
2110 )
2111 .unwrap();
2112}
2113
2114pub(crate) fn handle_is_selected(
2115 documents: &DocumentCollection,
2116 pipeline: PipelineId,
2117 element_id: String,
2118 reply: GenericSender<Result<bool, ErrorStatus>>,
2119) {
2120 reply
2121 .send(
2122 get_known_element(documents, pipeline, element_id).and_then(|element| {
2123 if let Some(input_element) = element.downcast::<HTMLInputElement>() {
2124 Ok(input_element.Checked())
2125 } else if let Some(option_element) = element.downcast::<HTMLOptionElement>() {
2126 Ok(option_element.Selected())
2127 } else if element.is::<HTMLElement>() {
2128 Ok(false) } else {
2130 Err(ErrorStatus::UnknownError)
2131 }
2132 }),
2133 )
2134 .unwrap();
2135}
2136
2137pub(crate) fn handle_add_load_status_sender(
2138 documents: &DocumentCollection,
2139 pipeline: PipelineId,
2140 reply: GenericSender<WebDriverLoadStatus>,
2141) {
2142 if let Some(document) = documents.find_document(pipeline) {
2143 let window = document.window();
2144 window.set_webdriver_load_status_sender(Some(reply));
2145 }
2146}
2147
2148pub(crate) fn handle_remove_load_status_sender(
2149 documents: &DocumentCollection,
2150 pipeline: PipelineId,
2151) {
2152 if let Some(document) = documents.find_document(pipeline) {
2153 let window = document.window();
2154 window.set_webdriver_load_status_sender(None);
2155 }
2156}
2157
2158fn scroll_into_view(cx: &mut JSContext, element: &Element) {
2160 let paint_tree = get_element_pointer_interactable_paint_tree(cx, element);
2162 if is_element_in_view(element, &paint_tree) {
2163 return;
2164 }
2165
2166 let options = BooleanOrScrollIntoViewOptions::ScrollIntoViewOptions(ScrollIntoViewOptions {
2171 parent: ScrollOptions {
2172 behavior: ScrollBehavior::Instant,
2173 },
2174 block: ScrollLogicalPosition::End,
2175 inline: ScrollLogicalPosition::Nearest,
2176 container: Default::default(),
2177 });
2178 element.ScrollIntoView(cx, options);
2180}
2181
2182pub(crate) fn set_protocol_handler_automation_mode(
2183 documents: &DocumentCollection,
2184 pipeline: PipelineId,
2185 mode: CustomHandlersAutomationMode,
2186) {
2187 if let Some(document) = documents.find_document(pipeline) {
2188 document.set_protocol_handler_automation_mode(mode);
2189 }
2190}