mirror of
https://github.com/servo/servo.git
synced 2025-07-05 14:33:38 +01:00
All "Find Element ..." in [spec](https://w3c.github.io/webdriver/#find-element-from-element) has exactly same step has "Find Elements ...", except they extract the first element if there is any. We now reuse the handler instead of running numerous repetitive steps, same as what we did for ["Find Element from Shadow Root"](https://w3c.github.io/webdriver/#find-element-from-shadow-root) in #37578. This reduces binary size by 98KB for Release profile in Windows and improves maintainability. Testing: `./mach test-wpt -r .\tests\wpt\tests\webdriver\tests\classic\find_element* --product servodriver` --------- Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
1566 lines
54 KiB
Rust
1566 lines
54 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
use std::cmp;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::ffi::CString;
|
|
use std::ptr::NonNull;
|
|
|
|
use base::id::{BrowsingContextId, PipelineId};
|
|
use cookie::Cookie;
|
|
use embedder_traits::{WebDriverFrameId, WebDriverJSError, WebDriverJSResult, WebDriverJSValue};
|
|
use euclid::default::{Point2D, Rect, Size2D};
|
|
use hyper_serde::Serde;
|
|
use ipc_channel::ipc::{self, IpcSender};
|
|
use js::jsapi::{
|
|
self, GetPropertyKeys, HandleValueArray, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
|
|
JS_IsExceptionPending, JSAutoRealm, JSContext, JSType, PropertyDescriptor,
|
|
};
|
|
use js::jsval::UndefinedValue;
|
|
use js::rust::wrappers::{JS_CallFunctionName, JS_GetProperty, JS_HasOwnProperty, JS_TypeOfValue};
|
|
use js::rust::{HandleObject, HandleValue, IdVector, ToString};
|
|
use net_traits::CookieSource::{HTTP, NonHTTP};
|
|
use net_traits::CoreResourceMsg::{
|
|
DeleteCookie, DeleteCookies, GetCookiesDataForUrl, SetCookieForUrl,
|
|
};
|
|
use net_traits::IpcSend;
|
|
use script_bindings::codegen::GenericBindings::ShadowRootBinding::ShadowRootMethods;
|
|
use script_bindings::conversions::is_array_like;
|
|
use servo_url::ServoUrl;
|
|
use webdriver::common::{WebElement, WebFrame, WebWindow};
|
|
use webdriver::error::ErrorStatus;
|
|
|
|
use crate::document_collection::DocumentCollection;
|
|
use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
|
|
use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
|
|
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
|
|
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
|
|
use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods;
|
|
use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
|
|
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
|
|
use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializerMethods;
|
|
use crate::dom::bindings::conversions::{
|
|
ConversionBehavior, ConversionResult, FromJSValConvertible, StringificationBehavior,
|
|
get_property, get_property_jsval, jsid_to_string, jsstring_to_str, root_from_object,
|
|
};
|
|
use crate::dom::bindings::error::{Error, report_pending_exception, throw_dom_exception};
|
|
use crate::dom::bindings::inheritance::Castable;
|
|
use crate::dom::bindings::reflector::{DomGlobal, DomObject};
|
|
use crate::dom::bindings::root::DomRoot;
|
|
use crate::dom::bindings::str::DOMString;
|
|
use crate::dom::document::Document;
|
|
use crate::dom::element::Element;
|
|
use crate::dom::eventtarget::EventTarget;
|
|
use crate::dom::globalscope::GlobalScope;
|
|
use crate::dom::htmldatalistelement::HTMLDataListElement;
|
|
use crate::dom::htmlelement::HTMLElement;
|
|
use crate::dom::htmliframeelement::HTMLIFrameElement;
|
|
use crate::dom::htmlinputelement::{HTMLInputElement, InputType};
|
|
use crate::dom::htmloptgroupelement::HTMLOptGroupElement;
|
|
use crate::dom::htmloptionelement::HTMLOptionElement;
|
|
use crate::dom::htmlselectelement::HTMLSelectElement;
|
|
use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
|
|
use crate::dom::nodelist::NodeList;
|
|
use crate::dom::types::ShadowRoot;
|
|
use crate::dom::window::Window;
|
|
use crate::dom::xmlserializer::XMLSerializer;
|
|
use crate::realms::{AlreadyInRealm, InRealm, enter_realm};
|
|
use crate::script_module::ScriptFetchOptions;
|
|
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
|
|
use crate::script_thread::ScriptThread;
|
|
|
|
fn is_stale(element: &Element) -> bool {
|
|
// An element is stale if its node document is not the active document
|
|
// or if it is not connected.
|
|
!element.owner_document().is_active() || !element.is_connected()
|
|
}
|
|
|
|
/// <https://w3c.github.io/webdriver/#dfn-get-a-known-shadow-root>
|
|
fn get_known_shadow_root(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
) -> Result<DomRoot<ShadowRoot>, ErrorStatus> {
|
|
let doc = documents
|
|
.find_document(pipeline)
|
|
.ok_or(ErrorStatus::NoSuchWindow)?;
|
|
// Step 1. If not node reference is known with session, session's current browsing context,
|
|
// and reference return error with error code no such shadow root.
|
|
if !ScriptThread::has_node_id(pipeline, &node_id) {
|
|
return Err(ErrorStatus::NoSuchShadowRoot);
|
|
}
|
|
|
|
// Step 2. Let node be the result of get a node with session,
|
|
// session's current browsing context, and reference.
|
|
let node = find_node_by_unique_id_in_document(&doc, node_id);
|
|
|
|
// Step 3. If node is not null and node does not implement ShadowRoot
|
|
// return error with error code no such shadow root.
|
|
if let Some(ref node) = node {
|
|
if !node.is::<ShadowRoot>() {
|
|
return Err(ErrorStatus::NoSuchShadowRoot);
|
|
}
|
|
}
|
|
|
|
// Step 4.1. If node is null return error with error code detached shadow root.
|
|
let Some(node) = node else {
|
|
return Err(ErrorStatus::DetachedShadowRoot);
|
|
};
|
|
|
|
// Step 4.2. If node is detached return error with error code detached shadow root.
|
|
// A shadow root is detached if its node document is not the active document
|
|
// or if the element node referred to as its host is stale.
|
|
let shadow_root = DomRoot::downcast::<ShadowRoot>(node).unwrap();
|
|
if !shadow_root.owner_document().is_active() {
|
|
return Err(ErrorStatus::DetachedShadowRoot);
|
|
}
|
|
|
|
let host = shadow_root.Host();
|
|
if is_stale(&host) {
|
|
return Err(ErrorStatus::DetachedShadowRoot);
|
|
}
|
|
// Step 5. Return success with data node.
|
|
Ok(shadow_root)
|
|
}
|
|
|
|
/// <https://w3c.github.io/webdriver/#dfn-get-a-known-element>
|
|
fn get_known_element(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
) -> Result<DomRoot<Element>, ErrorStatus> {
|
|
let doc = documents
|
|
.find_document(pipeline)
|
|
.ok_or(ErrorStatus::NoSuchWindow)?;
|
|
// Step 1. If not node reference is known with session, session's current browsing context,
|
|
// and reference return error with error code no such element.
|
|
if !ScriptThread::has_node_id(pipeline, &node_id) {
|
|
return Err(ErrorStatus::NoSuchElement);
|
|
}
|
|
// Step 2.Let node be the result of get a node with session,
|
|
// session's current browsing context, and reference.
|
|
let node = find_node_by_unique_id_in_document(&doc, node_id);
|
|
|
|
// Step 3. If node is not null and node does not implement Element
|
|
// return error with error code no such element.
|
|
if let Some(ref node) = node {
|
|
if !node.is::<Element>() {
|
|
return Err(ErrorStatus::NoSuchElement);
|
|
}
|
|
}
|
|
// Step 4.1. If node is null return error with error code stale element reference.
|
|
let Some(node) = node else {
|
|
return Err(ErrorStatus::StaleElementReference);
|
|
};
|
|
// Step 4.2. If node is stale return error with error code stale element reference.
|
|
let element = DomRoot::downcast::<Element>(node).unwrap();
|
|
if is_stale(&element) {
|
|
return Err(ErrorStatus::StaleElementReference);
|
|
}
|
|
// Step 5. Return success with data node.
|
|
Ok(element)
|
|
}
|
|
|
|
// This is also used by `dom/window.rs`
|
|
pub(crate) fn find_node_by_unique_id_in_document(
|
|
document: &Document,
|
|
node_id: String,
|
|
) -> Option<DomRoot<Node>> {
|
|
let pipeline = document.window().pipeline_id();
|
|
document
|
|
.upcast::<Node>()
|
|
.traverse_preorder(ShadowIncluding::Yes)
|
|
.find(|node| node.unique_id(pipeline) == node_id)
|
|
}
|
|
|
|
/// <https://w3c.github.io/webdriver/#dfn-link-text-selector>
|
|
fn matching_links(
|
|
links: &NodeList,
|
|
link_text: String,
|
|
partial: bool,
|
|
can_gc: CanGc,
|
|
) -> impl Iterator<Item = String> + '_ {
|
|
links
|
|
.iter()
|
|
.filter(move |node| {
|
|
let content = node
|
|
.downcast::<HTMLElement>()
|
|
.map(|element| element.InnerText(can_gc))
|
|
.map_or("".to_owned(), String::from)
|
|
.trim()
|
|
.to_owned();
|
|
if partial {
|
|
content.contains(&link_text)
|
|
} else {
|
|
content == link_text
|
|
}
|
|
})
|
|
.map(|node| node.unique_id(node.owner_doc().window().pipeline_id()))
|
|
}
|
|
|
|
fn all_matching_links(
|
|
root_node: &Node,
|
|
link_text: String,
|
|
partial: bool,
|
|
can_gc: CanGc,
|
|
) -> Result<Vec<String>, ErrorStatus> {
|
|
// <https://w3c.github.io/webdriver/#dfn-find>
|
|
// Step 7.2. If a DOMException, SyntaxError, XPathException, or other error occurs
|
|
// during the execution of the element location strategy, return error invalid selector.
|
|
root_node
|
|
.query_selector_all(DOMString::from("a"))
|
|
.map_err(|_| ErrorStatus::InvalidSelector)
|
|
.map(|nodes| matching_links(&nodes, link_text, partial, can_gc).collect())
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
unsafe fn object_has_to_json_property(
|
|
cx: *mut JSContext,
|
|
global_scope: &GlobalScope,
|
|
object: HandleObject,
|
|
) -> bool {
|
|
let name = CString::new("toJSON").unwrap();
|
|
let mut found = false;
|
|
if JS_HasOwnProperty(cx, object, name.as_ptr(), &mut found) && found {
|
|
rooted!(in(cx) let mut value = UndefinedValue());
|
|
let result = JS_GetProperty(cx, object, name.as_ptr(), value.handle_mut());
|
|
if !result {
|
|
throw_dom_exception(
|
|
SafeJSContext::from_ptr(cx),
|
|
global_scope,
|
|
Error::JSFailed,
|
|
CanGc::note(),
|
|
);
|
|
false
|
|
} else {
|
|
result && JS_TypeOfValue(cx, value.handle()) == JSType::JSTYPE_FUNCTION
|
|
}
|
|
} else if JS_IsExceptionPending(cx) {
|
|
throw_dom_exception(
|
|
SafeJSContext::from_ptr(cx),
|
|
global_scope,
|
|
Error::JSFailed,
|
|
CanGc::note(),
|
|
);
|
|
false
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
/// <https://w3c.github.io/webdriver/#dfn-collection>
|
|
unsafe fn is_arguments_object(cx: *mut JSContext, value: HandleValue) -> bool {
|
|
rooted!(in(cx) let class_name = ToString(cx, value));
|
|
let Some(class_name) = NonNull::new(class_name.get()) else {
|
|
return false;
|
|
};
|
|
jsstring_to_str(cx, class_name) == "[object Arguments]"
|
|
}
|
|
|
|
#[derive(Clone, Eq, Hash, PartialEq)]
|
|
struct HashableJSVal(u64);
|
|
|
|
impl From<HandleValue<'_>> for HashableJSVal {
|
|
fn from(v: HandleValue<'_>) -> HashableJSVal {
|
|
HashableJSVal(v.get().asBits_)
|
|
}
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
/// <https://w3c.github.io/webdriver/#dfn-json-deserialize>
|
|
pub(crate) fn jsval_to_webdriver(
|
|
cx: SafeJSContext,
|
|
global_scope: &GlobalScope,
|
|
val: HandleValue,
|
|
realm: InRealm,
|
|
can_gc: CanGc,
|
|
) -> WebDriverJSResult {
|
|
let mut seen = HashSet::new();
|
|
let result = unsafe { jsval_to_webdriver_inner(*cx, global_scope, val, &mut seen) };
|
|
if result.is_err() {
|
|
report_pending_exception(cx, true, realm, can_gc);
|
|
}
|
|
result
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
unsafe fn jsval_to_webdriver_inner(
|
|
cx: *mut JSContext,
|
|
global_scope: &GlobalScope,
|
|
val: HandleValue,
|
|
seen: &mut HashSet<HashableJSVal>,
|
|
) -> WebDriverJSResult {
|
|
let _ac = enter_realm(global_scope);
|
|
if val.get().is_undefined() {
|
|
Ok(WebDriverJSValue::Undefined)
|
|
} else if val.get().is_null() {
|
|
Ok(WebDriverJSValue::Null)
|
|
} else if val.get().is_boolean() {
|
|
Ok(WebDriverJSValue::Boolean(val.get().to_boolean()))
|
|
} else if val.get().is_int32() {
|
|
Ok(WebDriverJSValue::Int(
|
|
match FromJSValConvertible::from_jsval(cx, val, ConversionBehavior::Default).unwrap() {
|
|
ConversionResult::Success(c) => c,
|
|
_ => unreachable!(),
|
|
},
|
|
))
|
|
} else if val.get().is_double() {
|
|
Ok(WebDriverJSValue::Number(
|
|
match FromJSValConvertible::from_jsval(cx, val, ()).unwrap() {
|
|
ConversionResult::Success(c) => c,
|
|
_ => unreachable!(),
|
|
},
|
|
))
|
|
} else if val.get().is_string() {
|
|
//FIXME: use jsstring_to_str when jsval grows to_jsstring
|
|
let string: DOMString =
|
|
match FromJSValConvertible::from_jsval(cx, val, StringificationBehavior::Default)
|
|
.unwrap()
|
|
{
|
|
ConversionResult::Success(c) => c,
|
|
_ => unreachable!(),
|
|
};
|
|
Ok(WebDriverJSValue::String(String::from(string)))
|
|
}
|
|
// https://w3c.github.io/webdriver/#dfn-clone-an-object
|
|
else if val.get().is_object() {
|
|
let hashable = val.into();
|
|
// Step 1. If value is in `seen`, return error with error code javascript error.
|
|
if seen.contains(&hashable) {
|
|
return Err(WebDriverJSError::JSError);
|
|
}
|
|
//Step 2. Append value to `seen`.
|
|
seen.insert(hashable.clone());
|
|
|
|
rooted!(in(cx) let object = match FromJSValConvertible::from_jsval(cx, val, ()).unwrap() {
|
|
ConversionResult::Success(object) => object,
|
|
_ => unreachable!(),
|
|
});
|
|
let _ac = JSAutoRealm::new(cx, *object);
|
|
|
|
let return_val = if is_array_like::<crate::DomTypeHolder>(cx, val) ||
|
|
is_arguments_object(cx, val)
|
|
{
|
|
let mut result: Vec<WebDriverJSValue> = Vec::new();
|
|
|
|
let length = match get_property::<u32>(
|
|
cx,
|
|
object.handle(),
|
|
"length",
|
|
ConversionBehavior::Default,
|
|
) {
|
|
Ok(length) => match length {
|
|
Some(length) => length,
|
|
_ => return Err(WebDriverJSError::UnknownType),
|
|
},
|
|
Err(error) => {
|
|
throw_dom_exception(
|
|
SafeJSContext::from_ptr(cx),
|
|
global_scope,
|
|
error,
|
|
CanGc::note(),
|
|
);
|
|
return Err(WebDriverJSError::JSError);
|
|
},
|
|
};
|
|
// Step 4. For each enumerable property in value, run the following substeps:
|
|
for i in 0..length {
|
|
rooted!(in(cx) let mut item = UndefinedValue());
|
|
match get_property_jsval(cx, object.handle(), &i.to_string(), item.handle_mut()) {
|
|
Ok(_) => {
|
|
match jsval_to_webdriver_inner(cx, global_scope, item.handle(), seen) {
|
|
Ok(converted_item) => result.push(converted_item),
|
|
err @ Err(_) => return err,
|
|
}
|
|
},
|
|
Err(error) => {
|
|
throw_dom_exception(
|
|
SafeJSContext::from_ptr(cx),
|
|
global_scope,
|
|
error,
|
|
CanGc::note(),
|
|
);
|
|
return Err(WebDriverJSError::JSError);
|
|
},
|
|
}
|
|
}
|
|
Ok(WebDriverJSValue::ArrayLike(result))
|
|
} else if let Ok(element) = root_from_object::<Element>(*object, cx) {
|
|
Ok(WebDriverJSValue::Element(WebElement(
|
|
element
|
|
.upcast::<Node>()
|
|
.unique_id(element.owner_document().window().pipeline_id()),
|
|
)))
|
|
} else if let Ok(window) = root_from_object::<Window>(*object, cx) {
|
|
let window_proxy = window.window_proxy();
|
|
if window_proxy.is_browsing_context_discarded() {
|
|
return Err(WebDriverJSError::StaleElementReference);
|
|
} else {
|
|
let pipeline = window.pipeline_id();
|
|
if window_proxy.browsing_context_id() == window_proxy.webview_id() {
|
|
Ok(WebDriverJSValue::Window(WebWindow(
|
|
window.Document().upcast::<Node>().unique_id(pipeline),
|
|
)))
|
|
} else {
|
|
Ok(WebDriverJSValue::Frame(WebFrame(
|
|
window.Document().upcast::<Node>().unique_id(pipeline),
|
|
)))
|
|
}
|
|
}
|
|
} else if object_has_to_json_property(cx, global_scope, object.handle()) {
|
|
let name = CString::new("toJSON").unwrap();
|
|
rooted!(in(cx) let mut value = UndefinedValue());
|
|
if JS_CallFunctionName(
|
|
cx,
|
|
object.handle(),
|
|
name.as_ptr(),
|
|
&HandleValueArray::empty(),
|
|
value.handle_mut(),
|
|
) {
|
|
Ok(jsval_to_webdriver_inner(
|
|
cx,
|
|
global_scope,
|
|
value.handle(),
|
|
seen,
|
|
)?)
|
|
} else {
|
|
throw_dom_exception(
|
|
SafeJSContext::from_ptr(cx),
|
|
global_scope,
|
|
Error::JSFailed,
|
|
CanGc::note(),
|
|
);
|
|
return Err(WebDriverJSError::JSError);
|
|
}
|
|
} else {
|
|
let mut result = HashMap::new();
|
|
|
|
let mut ids = IdVector::new(cx);
|
|
if !GetPropertyKeys(
|
|
cx,
|
|
object.handle().into(),
|
|
jsapi::JSITER_OWNONLY,
|
|
ids.handle_mut(),
|
|
) {
|
|
return Err(WebDriverJSError::JSError);
|
|
}
|
|
for id in ids.iter() {
|
|
rooted!(in(cx) let id = *id);
|
|
rooted!(in(cx) let mut desc = PropertyDescriptor::default());
|
|
|
|
let mut is_none = false;
|
|
if !JS_GetOwnPropertyDescriptorById(
|
|
cx,
|
|
object.handle().into(),
|
|
id.handle().into(),
|
|
desc.handle_mut().into(),
|
|
&mut is_none,
|
|
) {
|
|
return Err(WebDriverJSError::JSError);
|
|
}
|
|
|
|
rooted!(in(cx) let mut property = UndefinedValue());
|
|
if !JS_GetPropertyById(
|
|
cx,
|
|
object.handle().into(),
|
|
id.handle().into(),
|
|
property.handle_mut().into(),
|
|
) {
|
|
return Err(WebDriverJSError::JSError);
|
|
}
|
|
if !property.is_undefined() {
|
|
let Some(name) = jsid_to_string(cx, id.handle()) else {
|
|
return Err(WebDriverJSError::JSError);
|
|
};
|
|
|
|
if let Ok(value) =
|
|
jsval_to_webdriver_inner(cx, global_scope, property.handle(), seen)
|
|
{
|
|
result.insert(name.into(), value);
|
|
} else {
|
|
return Err(WebDriverJSError::JSError);
|
|
}
|
|
}
|
|
}
|
|
Ok(WebDriverJSValue::Object(result))
|
|
};
|
|
// Step 5. Remove the last element of `seen`.
|
|
seen.remove(&hashable);
|
|
// Step 6. Return success with data `result`.
|
|
return_val
|
|
} else {
|
|
Err(WebDriverJSError::UnknownType)
|
|
}
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
pub(crate) fn handle_execute_script(
|
|
window: Option<DomRoot<Window>>,
|
|
eval: String,
|
|
reply: IpcSender<WebDriverJSResult>,
|
|
can_gc: CanGc,
|
|
) {
|
|
match window {
|
|
Some(window) => {
|
|
let cx = window.get_cx();
|
|
let realm = AlreadyInRealm::assert_for_cx(cx);
|
|
let realm = InRealm::already(&realm);
|
|
|
|
rooted!(in(*cx) let mut rval = UndefinedValue());
|
|
let global = window.as_global_scope();
|
|
let result = if global.evaluate_js_on_global_with_result(
|
|
&eval,
|
|
rval.handle_mut(),
|
|
ScriptFetchOptions::default_classic_script(global),
|
|
global.api_base_url(),
|
|
can_gc,
|
|
) {
|
|
jsval_to_webdriver(cx, global, rval.handle(), realm, can_gc)
|
|
} else {
|
|
Err(WebDriverJSError::JSError)
|
|
};
|
|
|
|
reply.send(result).unwrap();
|
|
},
|
|
None => {
|
|
reply
|
|
.send(Err(WebDriverJSError::BrowsingContextNotFound))
|
|
.unwrap();
|
|
},
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_execute_async_script(
|
|
window: Option<DomRoot<Window>>,
|
|
eval: String,
|
|
reply: IpcSender<WebDriverJSResult>,
|
|
can_gc: CanGc,
|
|
) {
|
|
match window {
|
|
Some(window) => {
|
|
let cx = window.get_cx();
|
|
let reply_sender = reply.clone();
|
|
window.set_webdriver_script_chan(Some(reply));
|
|
rooted!(in(*cx) let mut rval = UndefinedValue());
|
|
|
|
let global_scope = window.as_global_scope();
|
|
if !global_scope.evaluate_js_on_global_with_result(
|
|
&eval,
|
|
rval.handle_mut(),
|
|
ScriptFetchOptions::default_classic_script(global_scope),
|
|
global_scope.api_base_url(),
|
|
can_gc,
|
|
) {
|
|
reply_sender.send(Err(WebDriverJSError::JSError)).unwrap();
|
|
}
|
|
},
|
|
None => {
|
|
reply
|
|
.send(Err(WebDriverJSError::BrowsingContextNotFound))
|
|
.unwrap();
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get BrowsingContextId for <https://w3c.github.io/webdriver/#switch-to-parent-frame>
|
|
pub(crate) fn handle_get_parent_frame_id(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
reply: IpcSender<Result<BrowsingContextId, ErrorStatus>>,
|
|
) {
|
|
// Step 2. If session's current parent browsing context is no longer open,
|
|
// return error with error code no such window.
|
|
reply
|
|
.send(
|
|
documents
|
|
.find_window(pipeline)
|
|
.and_then(|window| {
|
|
window
|
|
.window_proxy()
|
|
.parent()
|
|
.map(|parent| parent.browsing_context_id())
|
|
})
|
|
.ok_or(ErrorStatus::NoSuchWindow),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Get the BrowsingContextId for <https://w3c.github.io/webdriver/#dfn-switch-to-frame>
|
|
pub(crate) fn handle_get_browsing_context_id(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
webdriver_frame_id: WebDriverFrameId,
|
|
reply: IpcSender<Result<BrowsingContextId, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(match webdriver_frame_id {
|
|
WebDriverFrameId::Short(id) => {
|
|
// Step 5. If id is not a supported property index of window,
|
|
// return error with error code no such frame.
|
|
documents
|
|
.find_document(pipeline)
|
|
.ok_or(ErrorStatus::NoSuchWindow)
|
|
.and_then(|document| {
|
|
document
|
|
.iframes()
|
|
.iter()
|
|
.nth(id as usize)
|
|
.and_then(|iframe| iframe.browsing_context_id())
|
|
.ok_or(ErrorStatus::NoSuchFrame)
|
|
})
|
|
},
|
|
WebDriverFrameId::Element(element_id) => {
|
|
get_known_element(documents, pipeline, element_id).and_then(|element| {
|
|
element
|
|
.downcast::<HTMLIFrameElement>()
|
|
.and_then(|element| element.browsing_context_id())
|
|
.ok_or(ErrorStatus::NoSuchFrame)
|
|
})
|
|
},
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
// https://w3c.github.io/webdriver/#dfn-center-point
|
|
fn get_element_in_view_center_point(element: &Element, can_gc: CanGc) -> Option<Point2D<i64>> {
|
|
element
|
|
.owner_document()
|
|
.GetBody()
|
|
.map(DomRoot::upcast::<Element>)
|
|
.and_then(|body| {
|
|
// Step 1: Let rectangle be the first element of the DOMRect sequence
|
|
// returned by calling getClientRects() on element.
|
|
element.GetClientRects(can_gc).first().map(|rectangle| {
|
|
let x = rectangle.X().round() as i64;
|
|
let y = rectangle.Y().round() as i64;
|
|
let width = rectangle.Width().round() as i64;
|
|
let height = rectangle.Height().round() as i64;
|
|
|
|
let client_width = body.ClientWidth(can_gc) as i64;
|
|
let client_height = body.ClientHeight(can_gc) as i64;
|
|
|
|
// Steps 2 - 5
|
|
let left = cmp::max(0, cmp::min(x, x + width));
|
|
let right = cmp::min(client_width, cmp::max(x, x + width));
|
|
let top = cmp::max(0, cmp::min(y, y + height));
|
|
let bottom = cmp::min(client_height, cmp::max(y, y + height));
|
|
|
|
// Steps 6 - 7
|
|
let x = (left + right) / 2;
|
|
let y = (top + bottom) / 2;
|
|
|
|
// Step 8
|
|
Point2D::new(x, y)
|
|
})
|
|
})
|
|
}
|
|
|
|
pub(crate) fn handle_get_element_in_view_center_point(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
reply: IpcSender<Result<Option<(i64, i64)>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).map(|element| {
|
|
get_element_in_view_center_point(&element, can_gc).map(|point| (point.x, point.y))
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn retrieve_document_and_check_root_existence(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
) -> Result<DomRoot<Document>, ErrorStatus> {
|
|
let document = documents
|
|
.find_document(pipeline)
|
|
.ok_or(ErrorStatus::NoSuchWindow)?;
|
|
|
|
// <https://w3c.github.io/webdriver/#find-element>
|
|
// <https://w3c.github.io/webdriver/#find-elements>
|
|
// Step 7 - 8. If current browsing context's document element is null,
|
|
// return error with error code no such element.
|
|
if document.GetDocumentElement().is_none() {
|
|
Err(ErrorStatus::NoSuchElement)
|
|
} else {
|
|
Ok(document)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_find_elements_css_selector(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
selector: String,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
) {
|
|
match retrieve_document_and_check_root_existence(documents, pipeline) {
|
|
Ok(document) => reply
|
|
.send(
|
|
document
|
|
.QuerySelectorAll(DOMString::from(selector))
|
|
.map_err(|_| ErrorStatus::InvalidSelector)
|
|
.map(|nodes| {
|
|
nodes
|
|
.iter()
|
|
.map(|x| x.upcast::<Node>().unique_id(pipeline))
|
|
.collect()
|
|
}),
|
|
)
|
|
.unwrap(),
|
|
Err(error) => reply.send(Err(error)).unwrap(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_find_elements_link_text(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
selector: String,
|
|
partial: bool,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
match retrieve_document_and_check_root_existence(documents, pipeline) {
|
|
Ok(document) => reply
|
|
.send(all_matching_links(
|
|
document.upcast::<Node>(),
|
|
selector.clone(),
|
|
partial,
|
|
can_gc,
|
|
))
|
|
.unwrap(),
|
|
Err(error) => reply.send(Err(error)).unwrap(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_find_elements_tag_name(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
selector: String,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
match retrieve_document_and_check_root_existence(documents, pipeline) {
|
|
Ok(document) => reply
|
|
.send(Ok(document
|
|
.GetElementsByTagName(DOMString::from(selector), can_gc)
|
|
.elements_iter()
|
|
.map(|x| x.upcast::<Node>().unique_id(pipeline))
|
|
.collect::<Vec<String>>()))
|
|
.unwrap(),
|
|
Err(error) => reply.send(Err(error)).unwrap(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_find_element_elements_css_selector(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
selector: String,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).and_then(|element| {
|
|
element
|
|
.upcast::<Node>()
|
|
.query_selector_all(DOMString::from(selector))
|
|
.map_err(|_| ErrorStatus::InvalidSelector)
|
|
.map(|nodes| {
|
|
nodes
|
|
.iter()
|
|
.map(|x| x.upcast::<Node>().unique_id(pipeline))
|
|
.collect()
|
|
})
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_find_element_elements_link_text(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
selector: String,
|
|
partial: bool,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).and_then(|element| {
|
|
all_matching_links(element.upcast::<Node>(), selector.clone(), partial, can_gc)
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_find_element_elements_tag_name(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
selector: String,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).map(|element| {
|
|
element
|
|
.GetElementsByTagName(DOMString::from(selector), can_gc)
|
|
.elements_iter()
|
|
.map(|x| x.upcast::<Node>().unique_id(pipeline))
|
|
.collect::<Vec<String>>()
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// <https://w3c.github.io/webdriver/#find-elements-from-shadow-root>
|
|
pub(crate) fn handle_find_shadow_elements_css_selector(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
shadow_root_id: String,
|
|
selector: String,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
|
|
shadow_root
|
|
.upcast::<Node>()
|
|
.query_selector_all(DOMString::from(selector))
|
|
.map_err(|_| ErrorStatus::InvalidSelector)
|
|
.map(|nodes| {
|
|
nodes
|
|
.iter()
|
|
.map(|x| x.upcast::<Node>().unique_id(pipeline))
|
|
.collect()
|
|
})
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_find_shadow_elements_link_text(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
shadow_root_id: String,
|
|
selector: String,
|
|
partial: bool,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
|
|
all_matching_links(
|
|
shadow_root.upcast::<Node>(),
|
|
selector.clone(),
|
|
partial,
|
|
can_gc,
|
|
)
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_find_shadow_elements_tag_name(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
shadow_root_id: String,
|
|
selector: String,
|
|
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
|
|
) {
|
|
// According to spec, we should use `getElementsByTagName`. But it is wrong, as only
|
|
// Document and Element implement this method. So we use `querySelectorAll` instead.
|
|
// But we should not return InvalidSelector error if the selector is not valid,
|
|
// as `getElementsByTagName` won't.
|
|
// See https://github.com/w3c/webdriver/issues/1903
|
|
reply
|
|
.send(
|
|
get_known_shadow_root(documents, pipeline, shadow_root_id).map(|shadow_root| {
|
|
shadow_root
|
|
.upcast::<Node>()
|
|
.query_selector_all(DOMString::from(selector))
|
|
.map(|nodes| {
|
|
nodes
|
|
.iter()
|
|
.map(|x| x.upcast::<Node>().unique_id(pipeline))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// <https://www.w3.org/TR/webdriver2/#dfn-get-element-shadow-root>
|
|
pub(crate) fn handle_get_element_shadow_root(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).map(|element| {
|
|
element
|
|
.GetShadowRoot()
|
|
.map(|x| x.upcast::<Node>().unique_id(pipeline))
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_will_send_keys(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
text: String,
|
|
strict_file_interactability: bool,
|
|
reply: IpcSender<Result<bool, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).and_then(|element| {
|
|
// Step 6: Let file be true if element is input element
|
|
// in the file upload state, or false otherwise
|
|
let file_input = element
|
|
.downcast::<HTMLInputElement>()
|
|
.filter(|&input_element| input_element.input_type() == InputType::File);
|
|
|
|
// Step 7: If file is false or the session's strict file interactability
|
|
if file_input.is_none() || strict_file_interactability {
|
|
match element.downcast::<HTMLElement>() {
|
|
Some(element) => {
|
|
// Need a way to find if this actually succeeded
|
|
element.Focus(can_gc);
|
|
},
|
|
None => return Err(ErrorStatus::UnknownError),
|
|
}
|
|
}
|
|
|
|
// Step 8 (file input)
|
|
if let Some(file_input) = file_input {
|
|
// Step 8.1: Let files be the result of splitting text
|
|
// on the newline (\n) character.
|
|
let files: Vec<DOMString> = text.split("\n").map(|s| s.into()).collect();
|
|
|
|
// Step 8.2
|
|
if files.is_empty() {
|
|
return Err(ErrorStatus::InvalidArgument);
|
|
}
|
|
|
|
// Step 8.3 - 8.4
|
|
if !file_input.Multiple() && files.len() > 1 {
|
|
return Err(ErrorStatus::InvalidArgument);
|
|
}
|
|
|
|
// Step 8.5
|
|
// TODO: Should return invalid argument error if file doesn't exist
|
|
|
|
// Step 8.6 - 8.7
|
|
// Input and change event already fired in `htmlinputelement.rs`.
|
|
file_input.SelectFiles(files, can_gc);
|
|
|
|
// Step 8.8
|
|
return Ok(false);
|
|
}
|
|
|
|
// TODO: Check non-typeable form control
|
|
// TODO: Check content editable
|
|
|
|
Ok(true)
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_active_element(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
reply: IpcSender<Option<String>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
documents
|
|
.find_document(pipeline)
|
|
.and_then(|document| document.GetActiveElement())
|
|
.map(|element| element.upcast::<Node>().unique_id(pipeline)),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_computed_role(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, node_id)
|
|
.map(|element| element.GetRole().map(String::from)),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_page_source(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
reply: IpcSender<Result<String, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
documents
|
|
.find_document(pipeline)
|
|
.ok_or(ErrorStatus::UnknownError)
|
|
.and_then(|document| match document.GetDocumentElement() {
|
|
Some(element) => match element.outer_html(can_gc) {
|
|
Ok(source) => Ok(source.to_string()),
|
|
Err(_) => {
|
|
match XMLSerializer::new(document.window(), None, can_gc)
|
|
.SerializeToString(element.upcast::<Node>())
|
|
{
|
|
Ok(source) => Ok(source.to_string()),
|
|
Err(_) => Err(ErrorStatus::UnknownError),
|
|
}
|
|
},
|
|
},
|
|
None => Err(ErrorStatus::UnknownError),
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_cookies(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
reply: IpcSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
// TODO: Return an error if the pipeline doesn't exist
|
|
match documents.find_document(pipeline) {
|
|
Some(document) => {
|
|
let url = document.url();
|
|
let (sender, receiver) = ipc::channel().unwrap();
|
|
let _ = document
|
|
.window()
|
|
.as_global_scope()
|
|
.resource_threads()
|
|
.send(GetCookiesDataForUrl(url, sender, NonHTTP));
|
|
Ok(receiver.recv().unwrap())
|
|
},
|
|
None => Ok(Vec::new()),
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
// https://w3c.github.io/webdriver/webdriver-spec.html#get-cookie
|
|
pub(crate) fn handle_get_cookie(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
name: String,
|
|
reply: IpcSender<Result<Vec<Serde<Cookie<'static>>>, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
// TODO: Return an error if the pipeline doesn't exist
|
|
match documents.find_document(pipeline) {
|
|
Some(document) => {
|
|
let url = document.url();
|
|
let (sender, receiver) = ipc::channel().unwrap();
|
|
let _ = document
|
|
.window()
|
|
.as_global_scope()
|
|
.resource_threads()
|
|
.send(GetCookiesDataForUrl(url, sender, NonHTTP));
|
|
let cookies = receiver.recv().unwrap();
|
|
Ok(cookies
|
|
.into_iter()
|
|
.filter(|cookie| cookie.name() == &*name)
|
|
.collect())
|
|
},
|
|
None => Ok(Vec::new()),
|
|
},
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
// https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie
|
|
pub(crate) fn handle_add_cookie(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
cookie: Cookie<'static>,
|
|
reply: IpcSender<Result<(), ErrorStatus>>,
|
|
) {
|
|
// TODO: Return a different error if the pipeline doesn't exist
|
|
let document = match documents.find_document(pipeline) {
|
|
Some(document) => document,
|
|
None => {
|
|
return reply.send(Err(ErrorStatus::UnableToSetCookie)).unwrap();
|
|
},
|
|
};
|
|
let url = document.url();
|
|
let method = if cookie.http_only().unwrap_or(false) {
|
|
HTTP
|
|
} else {
|
|
NonHTTP
|
|
};
|
|
|
|
let domain = cookie.domain().map(ToOwned::to_owned);
|
|
reply
|
|
.send(match (document.is_cookie_averse(), domain) {
|
|
(true, _) => Err(ErrorStatus::InvalidCookieDomain),
|
|
(false, Some(ref domain)) if url.host_str().map(|x| x == domain).unwrap_or(false) => {
|
|
let _ = document
|
|
.window()
|
|
.as_global_scope()
|
|
.resource_threads()
|
|
.send(SetCookieForUrl(url, Serde(cookie), method));
|
|
Ok(())
|
|
},
|
|
(false, None) => {
|
|
let _ = document
|
|
.window()
|
|
.as_global_scope()
|
|
.resource_threads()
|
|
.send(SetCookieForUrl(url, Serde(cookie), method));
|
|
Ok(())
|
|
},
|
|
(_, _) => Err(ErrorStatus::UnableToSetCookie),
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
// https://w3c.github.io/webdriver/#delete-all-cookies
|
|
pub(crate) fn handle_delete_cookies(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
reply: IpcSender<Result<(), ErrorStatus>>,
|
|
) {
|
|
let document = match documents.find_document(pipeline) {
|
|
Some(document) => document,
|
|
None => {
|
|
return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
|
|
},
|
|
};
|
|
let url = document.url();
|
|
document
|
|
.window()
|
|
.as_global_scope()
|
|
.resource_threads()
|
|
.send(DeleteCookies(url))
|
|
.unwrap();
|
|
reply.send(Ok(())).unwrap();
|
|
}
|
|
|
|
// https://w3c.github.io/webdriver/#delete-cookie
|
|
pub(crate) fn handle_delete_cookie(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
name: String,
|
|
reply: IpcSender<Result<(), ErrorStatus>>,
|
|
) {
|
|
let document = match documents.find_document(pipeline) {
|
|
Some(document) => document,
|
|
None => {
|
|
return reply.send(Err(ErrorStatus::UnknownError)).unwrap();
|
|
},
|
|
};
|
|
let url = document.url();
|
|
document
|
|
.window()
|
|
.as_global_scope()
|
|
.resource_threads()
|
|
.send(DeleteCookie(url, name))
|
|
.unwrap();
|
|
reply.send(Ok(())).unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_title(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
reply: IpcSender<String>,
|
|
) {
|
|
reply
|
|
.send(
|
|
// TODO: Return an error if the pipeline doesn't exist
|
|
documents
|
|
.find_document(pipeline)
|
|
.map(|document| String::from(document.Title()))
|
|
.unwrap_or_default(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_rect(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
reply: IpcSender<Result<Rect<f64>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).and_then(|element| {
|
|
// https://w3c.github.io/webdriver/webdriver-spec.html#dfn-calculate-the-absolute-position
|
|
match element.downcast::<HTMLElement>() {
|
|
Some(html_element) => {
|
|
// Step 1
|
|
let mut x = 0;
|
|
let mut y = 0;
|
|
|
|
let mut offset_parent = html_element.GetOffsetParent(can_gc);
|
|
|
|
// Step 2
|
|
while let Some(element) = offset_parent {
|
|
offset_parent = match element.downcast::<HTMLElement>() {
|
|
Some(elem) => {
|
|
x += elem.OffsetLeft(can_gc);
|
|
y += elem.OffsetTop(can_gc);
|
|
elem.GetOffsetParent(can_gc)
|
|
},
|
|
None => None,
|
|
};
|
|
}
|
|
// Step 3
|
|
Ok(Rect::new(
|
|
Point2D::new(x as f64, y as f64),
|
|
Size2D::new(
|
|
html_element.OffsetWidth(can_gc) as f64,
|
|
html_element.OffsetHeight(can_gc) as f64,
|
|
),
|
|
))
|
|
},
|
|
None => Err(ErrorStatus::UnknownError),
|
|
}
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_bounding_client_rect(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
reply: IpcSender<Result<Rect<f32>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).map(|element| {
|
|
let rect = element.GetBoundingClientRect(can_gc);
|
|
Rect::new(
|
|
Point2D::new(rect.X() as f32, rect.Y() as f32),
|
|
Size2D::new(rect.Width() as f32, rect.Height() as f32),
|
|
)
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// <https://w3c.github.io/webdriver/#dfn-get-element-text>
|
|
pub(crate) fn handle_get_text(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
reply: IpcSender<Result<String, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, node_id).map(|element| {
|
|
element
|
|
.downcast::<HTMLElement>()
|
|
.map(|htmlelement| htmlelement.InnerText(can_gc).to_string())
|
|
.unwrap_or_else(|| {
|
|
element
|
|
.upcast::<Node>()
|
|
.GetTextContent()
|
|
.map_or("".to_owned(), String::from)
|
|
})
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_name(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
reply: IpcSender<Result<String, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, node_id)
|
|
.map(|element| String::from(element.TagName())),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_attribute(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
name: String,
|
|
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, node_id).map(|element| {
|
|
element
|
|
.GetAttribute(DOMString::from(name))
|
|
.map(String::from)
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
pub(crate) fn handle_get_property(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
name: String,
|
|
reply: IpcSender<Result<WebDriverJSValue, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, node_id).map(|element| {
|
|
let document = documents.find_document(pipeline).unwrap();
|
|
let realm = enter_realm(&*document);
|
|
let cx = document.window().get_cx();
|
|
|
|
rooted!(in(*cx) let mut property = UndefinedValue());
|
|
match unsafe {
|
|
get_property_jsval(
|
|
*cx,
|
|
element.reflector().get_jsobject(),
|
|
&name,
|
|
property.handle_mut(),
|
|
)
|
|
} {
|
|
Ok(_) => {
|
|
match jsval_to_webdriver(
|
|
cx,
|
|
&element.global(),
|
|
property.handle(),
|
|
InRealm::entered(&realm),
|
|
can_gc,
|
|
) {
|
|
Ok(property) => property,
|
|
Err(_) => WebDriverJSValue::Undefined,
|
|
}
|
|
},
|
|
Err(error) => {
|
|
throw_dom_exception(cx, &element.global(), error, can_gc);
|
|
WebDriverJSValue::Undefined
|
|
},
|
|
}
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_css(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
node_id: String,
|
|
name: String,
|
|
reply: IpcSender<Result<String, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, node_id).map(|element| {
|
|
let window = element.owner_window();
|
|
String::from(
|
|
window
|
|
.GetComputedStyle(&element, None)
|
|
.GetPropertyValue(DOMString::from(name), can_gc),
|
|
)
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_get_url(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
reply: IpcSender<ServoUrl>,
|
|
_can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
// TODO: Return an error if the pipeline doesn't exist.
|
|
documents
|
|
.find_document(pipeline)
|
|
.map(|document| document.url())
|
|
.unwrap_or_else(|| ServoUrl::parse("about:blank").expect("infallible")),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn get_option_parent(node: &Node) -> Option<DomRoot<Node>> {
|
|
// Get parent for `<option>` or `<optiongrp>` based on container spec:
|
|
// > 1. Let datalist parent be the first datalist element reached by traversing the tree
|
|
// > in reverse order from element, or undefined if the root of the tree is reached.
|
|
// > 2. Let select parent be the first select element reached by traversing the tree in
|
|
// > reverse order from element, or undefined if the root of the tree is reached.
|
|
// > 3. If datalist parent is undefined, the element context is select parent.
|
|
// > Otherwise, the element context is datalist parent.
|
|
let root_node = node.GetRootNode(&GetRootNodeOptions::empty());
|
|
node.preceding_nodes(&root_node)
|
|
.find(|preceding| preceding.is::<HTMLDataListElement>())
|
|
.or_else(|| {
|
|
node.preceding_nodes(&root_node)
|
|
.find(|preceding| preceding.is::<HTMLSelectElement>())
|
|
})
|
|
}
|
|
|
|
// https://w3c.github.io/webdriver/#dfn-container
|
|
fn get_container(node: &Node) -> Option<DomRoot<Node>> {
|
|
if node.is::<HTMLOptionElement>() {
|
|
return get_option_parent(node);
|
|
}
|
|
if node.is::<HTMLOptGroupElement>() {
|
|
let option_parent = get_option_parent(node);
|
|
return option_parent.or_else(|| Some(DomRoot::from_ref(node)));
|
|
}
|
|
Some(DomRoot::from_ref(node))
|
|
}
|
|
|
|
// https://w3c.github.io/webdriver/#element-click
|
|
pub(crate) fn handle_element_click(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
|
|
can_gc: CanGc,
|
|
) {
|
|
reply
|
|
.send(
|
|
// Step 3
|
|
get_known_element(documents, pipeline, element_id).and_then(|element| {
|
|
// Step 4
|
|
if let Some(input_element) = element.downcast::<HTMLInputElement>() {
|
|
if input_element.input_type() == InputType::File {
|
|
return Err(ErrorStatus::InvalidArgument);
|
|
}
|
|
}
|
|
|
|
let Some(container) = get_container(element.upcast::<Node>()) else {
|
|
return Err(ErrorStatus::UnknownError);
|
|
};
|
|
|
|
// Step 5
|
|
// TODO: scroll into view
|
|
|
|
// Step 6
|
|
// TODO: return error if still not in view
|
|
|
|
// Step 7
|
|
// TODO: return error if obscured
|
|
|
|
// Step 8
|
|
match element.downcast::<HTMLOptionElement>() {
|
|
Some(option_element) => {
|
|
// Steps 8.2 - 8.4
|
|
let event_target = container.upcast::<EventTarget>();
|
|
event_target.fire_event(atom!("mouseover"), can_gc);
|
|
event_target.fire_event(atom!("mousemove"), can_gc);
|
|
event_target.fire_event(atom!("mousedown"), can_gc);
|
|
|
|
// Step 8.5
|
|
match container.downcast::<HTMLElement>() {
|
|
Some(html_element) => html_element.Focus(can_gc),
|
|
None => return Err(ErrorStatus::UnknownError),
|
|
}
|
|
|
|
// Step 8.6
|
|
if !option_element.Disabled() {
|
|
// Step 8.6.1
|
|
event_target.fire_event(atom!("input"), can_gc);
|
|
|
|
// Steps 8.6.2
|
|
let previous_selectedness = option_element.Selected();
|
|
|
|
// Step 8.6.3
|
|
match container.downcast::<HTMLSelectElement>() {
|
|
Some(select_element) => {
|
|
if select_element.Multiple() {
|
|
option_element.SetSelected(!option_element.Selected());
|
|
}
|
|
},
|
|
None => option_element.SetSelected(true),
|
|
}
|
|
|
|
// Step 8.6.4
|
|
if !previous_selectedness {
|
|
event_target.fire_event(atom!("change"), can_gc);
|
|
}
|
|
}
|
|
|
|
// Steps 8.7 - 8.8
|
|
event_target.fire_event(atom!("mouseup"), can_gc);
|
|
event_target.fire_event(atom!("click"), can_gc);
|
|
|
|
Ok(None)
|
|
},
|
|
None => Ok(Some(element.upcast::<Node>().unique_id(pipeline))),
|
|
}
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_is_enabled(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
reply: IpcSender<Result<bool, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id)
|
|
.map(|element| element.enabled_state()),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) fn handle_is_selected(
|
|
documents: &DocumentCollection,
|
|
pipeline: PipelineId,
|
|
element_id: String,
|
|
reply: IpcSender<Result<bool, ErrorStatus>>,
|
|
) {
|
|
reply
|
|
.send(
|
|
get_known_element(documents, pipeline, element_id).and_then(|element| {
|
|
if let Some(input_element) = element.downcast::<HTMLInputElement>() {
|
|
Ok(input_element.Checked())
|
|
} else if let Some(option_element) = element.downcast::<HTMLOptionElement>() {
|
|
Ok(option_element.Selected())
|
|
} else if element.is::<HTMLElement>() {
|
|
Ok(false) // regular elements are not selectable
|
|
} else {
|
|
Err(ErrorStatus::UnknownError)
|
|
}
|
|
}),
|
|
)
|
|
.unwrap();
|
|
}
|