servo/components/webdriver_server/lib.rs
Euclid Ye 6aa82309c3
webdriver: Wait animation frame callbacks before taking (element) screenshot (#39539)
According to spec, we should wait animation frame callbacks before
taking (element) screenshot. As "element screenshot" would automatically
scroll into view, this solves intermittency.

Testing: Manually tested on pages, and
`take_element_screenshot/scroll_into_view.py` passes stably now.
Fixes: #39306

---------

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
2025-09-27 13:29:44 +00:00

2760 lines
112 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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/. */
#![crate_name = "webdriver_server"]
#![crate_type = "rlib"]
#![deny(unsafe_code)]
mod actions;
mod capabilities;
mod elements;
mod session;
mod timeout;
mod user_prompt;
use std::borrow::ToOwned;
use std::cell::{Cell, LazyCell};
use std::collections::{BTreeMap, HashMap};
use std::io::Cursor;
use std::net::{SocketAddr, SocketAddrV4};
use std::time::Duration;
use std::{env, fmt, process, thread};
use base::generic_channel::{self, GenericSender, RoutedReceiver};
use base::id::{BrowsingContextId, WebViewId};
use base64::Engine;
use capabilities::ServoCapabilities;
use cookie::{CookieBuilder, Expiration, SameSite};
use crossbeam_channel::{Sender, after, select};
use embedder_traits::{
EventLoopWaker, JSValue, MouseButton, WebDriverCommandMsg, WebDriverCommandResponse,
WebDriverFrameId, WebDriverJSError, WebDriverJSResult, WebDriverLoadStatus, WebDriverMessageId,
WebDriverScriptCommand,
};
use euclid::{Point2D, Rect, Size2D};
use http::method::Method;
use image::{DynamicImage, ImageFormat, RgbaImage};
use ipc_channel::ipc::{self, IpcReceiver};
use keyboard_types::webdriver::{Event as DispatchStringEvent, KeyInputState, send_keys};
use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, NamedKey};
use log::{debug, error, info};
use pixels::PixelFormat;
use serde::de::{Deserializer, MapAccess, Visitor};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use servo_config::prefs::{self, PrefValue, Preferences};
use servo_geometry::DeviceIndependentIntRect;
use servo_url::ServoUrl;
use style_traits::CSSPixel;
use time::OffsetDateTime;
use uuid::Uuid;
use webdriver::actions::{
ActionSequence, ActionsType, KeyAction, KeyActionItem, KeyDownAction, KeyUpAction,
PointerAction, PointerActionItem, PointerActionParameters, PointerDownAction,
PointerMoveAction, PointerOrigin, PointerType, PointerUpAction,
};
use webdriver::capabilities::CapabilitiesMatching;
use webdriver::command::{
ActionsParameters, AddCookieParameters, GetParameters, JavascriptCommandParameters,
LocatorParameters, NewSessionParameters, NewWindowParameters, SendKeysParameters,
SwitchToFrameParameters, SwitchToWindowParameters, TimeoutsParameters, WebDriverCommand,
WebDriverExtensionCommand, WebDriverMessage, WindowRectParameters,
};
use webdriver::common::{
Cookie, Date, LocatorStrategy, Parameters, ShadowRoot, WebElement, WebFrame, WebWindow,
};
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
use webdriver::httpapi::WebDriverExtensionRoute;
use webdriver::response::{
CloseWindowResponse, CookieResponse, CookiesResponse, ElementRectResponse, NewSessionResponse,
NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse,
};
use webdriver::server::{self, Session, SessionTeardownKind, WebDriverHandler};
use crate::actions::{InputSourceState, PointerInputState};
use crate::session::{PageLoadStrategy, WebDriverSession};
#[derive(Default)]
pub struct WebDriverMessageIdGenerator {
counter: Cell<usize>,
}
impl WebDriverMessageIdGenerator {
pub fn new() -> Self {
Self {
counter: Cell::new(0),
}
}
/// Returns a unique ID.
pub fn next(&self) -> WebDriverMessageId {
let id = self.counter.get();
self.counter.set(id + 1);
WebDriverMessageId(id)
}
}
fn extension_routes() -> Vec<(Method, &'static str, ServoExtensionRoute)> {
vec![
(
Method::POST,
"/session/{sessionId}/servo/prefs/get",
ServoExtensionRoute::GetPrefs,
),
(
Method::POST,
"/session/{sessionId}/servo/prefs/set",
ServoExtensionRoute::SetPrefs,
),
(
Method::POST,
"/session/{sessionId}/servo/prefs/reset",
ServoExtensionRoute::ResetPrefs,
),
]
}
fn cookie_msg_to_cookie(cookie: cookie::Cookie) -> Cookie {
Cookie {
name: cookie.name().to_owned(),
value: cookie.value().to_owned(),
path: cookie.path().map(|s| s.to_owned()),
domain: cookie.domain().map(|s| s.to_owned()),
expiry: cookie.expires().and_then(|expiration| match expiration {
Expiration::DateTime(date_time) => Some(Date(date_time.unix_timestamp() as u64)),
Expiration::Session => None,
}),
secure: cookie.secure().unwrap_or(false),
http_only: cookie.http_only().unwrap_or(false),
same_site: cookie.same_site().map(|s| s.to_string()),
}
}
pub fn start_server(
port: u16,
embedder_sender: Sender<WebDriverCommandMsg>,
event_loop_waker: Box<dyn EventLoopWaker>,
webdriver_response_receiver: IpcReceiver<WebDriverCommandResponse>,
) {
let handler = Handler::new(
embedder_sender,
event_loop_waker,
webdriver_response_receiver,
);
thread::Builder::new()
.name("WebDriverHttpServer".to_owned())
.spawn(move || {
let address = SocketAddrV4::new("0.0.0.0".parse().unwrap(), port);
match server::start(
SocketAddr::V4(address),
vec![],
vec![],
handler,
extension_routes(),
) {
Ok(listening) => info!("WebDriver server listening on {}", listening.socket),
Err(_) => panic!("Unable to start WebDriver HTTPD server"),
}
})
.expect("Thread spawning failed");
}
struct Handler {
/// The threaded receiver on which we can block for a load-status.
/// It will receive messages sent on the load_status_sender,
/// and forwarded by the IPC router.
load_status_receiver: RoutedReceiver<WebDriverLoadStatus>,
/// The IPC sender which we can clone and pass along to the constellation,
/// for it to send us a load-status. Messages sent on it
/// will be forwarded to the load_status_receiver.
load_status_sender: GenericSender<WebDriverLoadStatus>,
session: Option<WebDriverSession>,
/// A [`Sender`] that sends messages to the embedder that this `WebDriver instance controls.
/// In addition to sending a message, we must always wake up the embedder's event loop so it
/// knows that more messages are available for processing.
embedder_sender: Sender<WebDriverCommandMsg>,
/// An [`EventLoopWaker`] which is used to wake up the embedder event loop.
event_loop_waker: Box<dyn EventLoopWaker>,
/// Receiver notification from the constellation when a command is completed
webdriver_response_receiver: IpcReceiver<WebDriverCommandResponse>,
id_generator: WebDriverMessageIdGenerator,
current_action_id: Cell<Option<WebDriverMessageId>>,
/// Number of pending actions of which WebDriver is waiting for responses.
num_pending_actions: Cell<u32>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[allow(clippy::enum_variant_names)]
enum ServoExtensionRoute {
GetPrefs,
SetPrefs,
ResetPrefs,
}
impl WebDriverExtensionRoute for ServoExtensionRoute {
type Command = ServoExtensionCommand;
fn command(
&self,
_parameters: &Parameters,
body_data: &Value,
) -> WebDriverResult<WebDriverCommand<ServoExtensionCommand>> {
let command = match *self {
ServoExtensionRoute::GetPrefs => {
let parameters: GetPrefsParameters = serde_json::from_value(body_data.clone())?;
ServoExtensionCommand::GetPrefs(parameters)
},
ServoExtensionRoute::SetPrefs => {
let parameters: SetPrefsParameters = serde_json::from_value(body_data.clone())?;
ServoExtensionCommand::SetPrefs(parameters)
},
ServoExtensionRoute::ResetPrefs => {
let parameters: GetPrefsParameters = serde_json::from_value(body_data.clone())?;
ServoExtensionCommand::ResetPrefs(parameters)
},
};
Ok(WebDriverCommand::Extension(command))
}
}
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::enum_variant_names)]
enum ServoExtensionCommand {
GetPrefs(GetPrefsParameters),
SetPrefs(SetPrefsParameters),
ResetPrefs(GetPrefsParameters),
}
impl WebDriverExtensionCommand for ServoExtensionCommand {
fn parameters_json(&self) -> Option<Value> {
match *self {
ServoExtensionCommand::GetPrefs(ref x) => serde_json::to_value(x).ok(),
ServoExtensionCommand::SetPrefs(ref x) => serde_json::to_value(x).ok(),
ServoExtensionCommand::ResetPrefs(ref x) => serde_json::to_value(x).ok(),
}
}
}
#[derive(Clone)]
struct SendableJSValue(pub JSValue);
impl Serialize for SendableJSValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.0 {
JSValue::Undefined | JSValue::Null => serializer.serialize_unit(),
JSValue::Boolean(x) => serializer.serialize_bool(x),
JSValue::Number(x) => {
if x.fract() == 0.0 {
serializer.serialize_i64(x as i64)
} else {
serializer.serialize_f64(x)
}
},
JSValue::String(ref x) => serializer.serialize_str(x),
JSValue::Element(ref x) => WebElement(x.clone()).serialize(serializer),
JSValue::ShadowRoot(ref x) => ShadowRoot(x.clone()).serialize(serializer),
JSValue::Frame(ref x) => WebFrame(x.clone()).serialize(serializer),
JSValue::Window(ref x) => WebWindow(x.clone()).serialize(serializer),
JSValue::Array(ref x) => x
.iter()
.map(|element| SendableJSValue(element.clone()))
.collect::<Vec<SendableJSValue>>()
.serialize(serializer),
JSValue::Object(ref x) => x
.iter()
.map(|(k, v)| (k.clone(), SendableJSValue(v.clone())))
.collect::<HashMap<String, SendableJSValue>>()
.serialize(serializer),
}
}
}
#[derive(Clone, Debug, PartialEq)]
struct WebDriverPrefValue(pub PrefValue);
impl Serialize for WebDriverPrefValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.0 {
PrefValue::Bool(b) => serializer.serialize_bool(b),
PrefValue::Str(ref s) => serializer.serialize_str(s),
PrefValue::Float(f) => serializer.serialize_f64(f),
PrefValue::Int(i) => serializer.serialize_i64(i),
PrefValue::Array(ref v) => v
.iter()
.map(|value| WebDriverPrefValue(value.clone()))
.collect::<Vec<WebDriverPrefValue>>()
.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for WebDriverPrefValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl ::serde::de::Visitor<'_> for Visitor {
type Value = WebDriverPrefValue;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("preference value")
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: ::serde::de::Error,
{
Ok(WebDriverPrefValue(PrefValue::Float(value)))
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: ::serde::de::Error,
{
Ok(WebDriverPrefValue(PrefValue::Int(value)))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: ::serde::de::Error,
{
Ok(WebDriverPrefValue(PrefValue::Int(value as i64)))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: ::serde::de::Error,
{
Ok(WebDriverPrefValue(PrefValue::Str(String::from(value))))
}
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
where
E: ::serde::de::Error,
{
Ok(WebDriverPrefValue(PrefValue::Bool(value)))
}
}
deserializer.deserialize_any(Visitor)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
struct GetPrefsParameters {
prefs: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
struct SetPrefsParameters {
#[serde(deserialize_with = "map_to_vec")]
prefs: Vec<(String, WebDriverPrefValue)>,
}
fn map_to_vec<'de, D>(de: D) -> Result<Vec<(String, WebDriverPrefValue)>, D::Error>
where
D: Deserializer<'de>,
{
de.deserialize_map(TupleVecMapVisitor)
}
struct TupleVecMapVisitor;
impl<'de> Visitor<'de> for TupleVecMapVisitor {
type Value = Vec<(String, WebDriverPrefValue)>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map")
}
#[inline]
fn visit_unit<E>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
#[inline]
fn visit_map<T>(self, mut access: T) -> Result<Self::Value, T::Error>
where
T: MapAccess<'de>,
{
let mut values = Vec::new();
while let Some((key, value)) = access.next_entry()? {
values.push((key, value));
}
Ok(values)
}
}
enum VerifyBrowsingContextIsOpen {
Yes,
No,
}
impl Handler {
pub fn new(
embedder_sender: Sender<WebDriverCommandMsg>,
event_loop_waker: Box<dyn EventLoopWaker>,
webdriver_response_receiver: IpcReceiver<WebDriverCommandResponse>,
) -> Handler {
// Create a pair of both an IPC and a threaded channel,
// keep the IPC sender to clone and pass to the constellation for each load,
// and keep a threaded receiver to block on an incoming load-status.
// Pass the others to the IPC router so that IPC messages are forwarded to the threaded receiver.
// We need to use the router because IPC does not come with a timeout on receive/select.
let (load_status_sender, receiver) = generic_channel::channel().unwrap();
let load_status_receiver = receiver.route_preserving_errors();
Handler {
load_status_sender,
load_status_receiver,
session: None,
embedder_sender,
event_loop_waker,
webdriver_response_receiver,
id_generator: WebDriverMessageIdGenerator::new(),
current_action_id: Cell::new(None),
num_pending_actions: Cell::new(0),
}
}
pub fn browsing_context_id(&self) -> WebDriverResult<BrowsingContextId> {
self.session()?
.current_browsing_context_id()
.ok_or_else(|| {
WebDriverError::new(ErrorStatus::UnknownError, "No browsing context available")
})
}
pub fn webview_id(&self) -> WebDriverResult<WebViewId> {
self.session()?
.current_webview_id()
.ok_or_else(|| WebDriverError::new(ErrorStatus::UnknownError, "No webview available"))
}
fn increment_num_pending_actions(&self) {
// Increase the num_pending_actions by one every time we dispatch non null actions.
self.num_pending_actions
.set(self.num_pending_actions.get() + 1);
}
fn send_message_to_embedder(&self, msg: WebDriverCommandMsg) -> WebDriverResult<()> {
self.embedder_sender.send(msg).map_err(|_| {
WebDriverError::new(
ErrorStatus::UnknownError,
"Failed to send message to embedder",
)
})?;
self.event_loop_waker.wake();
Ok(())
}
fn add_load_status_sender(&self) -> WebDriverResult<()> {
self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
self.browsing_context_id()?,
WebDriverScriptCommand::AddLoadStatusSender(
self.webview_id()?,
self.load_status_sender.clone(),
),
))
}
fn clear_load_status_sender(&self) -> WebDriverResult<()> {
self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
self.browsing_context_id()?,
WebDriverScriptCommand::RemoveLoadStatusSender(self.webview_id()?),
))
}
// This function is called only if session and webview are verified.
fn verified_webview_id(&self) -> WebViewId {
self.session().unwrap().current_webview_id().unwrap()
}
fn focused_webview_id(&self) -> WebDriverResult<Option<WebViewId>> {
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::GetFocusedWebView(sender.clone()))?;
// Wait until the document is ready before returning the top-level browsing context id.
wait_for_ipc_response(receiver)
}
fn session(&self) -> WebDriverResult<&WebDriverSession> {
match self.session {
Some(ref x) => Ok(x),
// https://w3c.github.io/webdriver/#ref-for-dfn-invalid-session-id-1
None => Err(WebDriverError::new(
ErrorStatus::InvalidSessionId,
"Session not created",
)),
}
}
fn session_mut(&mut self) -> WebDriverResult<&mut WebDriverSession> {
match self.session {
Some(ref mut x) => Ok(x),
// https://w3c.github.io/webdriver/#ref-for-dfn-invalid-session-id-1
None => Err(WebDriverError::new(
ErrorStatus::InvalidSessionId,
"Session not created",
)),
}
}
/// <https://w3c.github.io/webdriver/#new-session>
fn handle_new_session(
&mut self,
parameters: &NewSessionParameters,
) -> WebDriverResult<WebDriverResponse> {
if let Ok(value) = env::var("DELAY_AFTER_ACCEPT") {
let seconds = value.parse::<u64>().unwrap_or_default();
println!("Waiting for {} seconds...", seconds);
println!("lldb -p {}", process::id());
thread::sleep(Duration::from_secs(seconds));
}
// Step 1. If the list of active HTTP sessions is not empty
// return error with error code session not created.
if self.session.is_some() {
return Err(WebDriverError::new(
ErrorStatus::SessionNotCreated,
"Session already created",
));
}
// Step 2. Skip because the step is only applied to an intermediary node.
// Step 3. Skip since all sessions are http for now.
// Step 4. Let capabilities be the result of trying to process capabilities
let mut servo_capabilities = ServoCapabilities::new();
let processed_capabilities = parameters.match_browser(&mut servo_capabilities)?;
// Step 5. If capabilities's is null, return error with error code session not created.
let mut capabilities = match processed_capabilities {
Some(capabilities) => capabilities,
None => {
return Err(WebDriverError::new(
ErrorStatus::SessionNotCreated,
"Session not created due to invalid capabilities",
));
},
};
// Step 6. Create a session
let session_id = self.create_session(&mut capabilities, &servo_capabilities)?;
// Step 7. Let response be a JSON Object initialized with session's session ID and capabilities
let response = NewSessionResponse::new(session_id.to_string(), Value::Object(capabilities));
// Step 8. Set session' current top-level browsing context
match self.focused_webview_id()? {
Some(webview_id) => {
self.session_mut()?.set_webview_id(webview_id);
self.session_mut()?
.set_browsing_context_id(BrowsingContextId::from(webview_id));
},
None => {
// This happens when there is no open webview.
// We need to create a new one. See https://github.com/servo/servo/issues/37408
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::NewWebView(
sender,
Some(self.load_status_sender.clone()),
))?;
let webview_id = receiver
.recv()
.expect("IPC failure when creating new webview for new session");
self.focus_webview(webview_id)?;
self.session_mut()?.set_webview_id(webview_id);
self.session_mut()?
.set_browsing_context_id(BrowsingContextId::from(webview_id));
let _ = self.wait_document_ready(3000);
},
};
// Step 9. Set the request queue to a new queue.
// Skip here because the requests are handled in the external crate.
// Step 10. Return success with data body
Ok(WebDriverResponse::NewSession(response))
}
/// <https://w3c.github.io/webdriver/#dfn-delete-session>
fn handle_delete_session(&mut self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session is http, close the session
self.session = None;
// Step 2. Return success with data null
Ok(WebDriverResponse::DeleteSession)
}
/// <https://w3c.github.io/webdriver/#status>
fn handle_status(&self) -> WebDriverResult<WebDriverResponse> {
Ok(WebDriverResponse::Generic(ValueResponse(
if self.session.is_none() {
json!({ "ready": true, "message": "Ready for a new session" })
} else {
json!({ "ready": false, "message": "Not ready for a new session" })
},
)))
}
/// Send command to Script Thread with session's current browsing context.
/// If `verify` is [`VerifyBrowsingContextIsOpen::Yes`],
/// it would verify the existence of browsing context before sending.
fn browsing_context_script_command(
&self,
cmd_msg: WebDriverScriptCommand,
verify: VerifyBrowsingContextIsOpen,
) -> WebDriverResult<()> {
let browsing_context_id = self.browsing_context_id()?;
if let VerifyBrowsingContextIsOpen::Yes = verify {
self.verify_browsing_context_is_open(browsing_context_id)?;
}
self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
browsing_context_id,
cmd_msg,
))?;
Ok(())
}
/// Send command to Script Thread with session's current top-level browsing context.
/// If `verify` is [`VerifyBrowsingContextIsOpen::Yes`],
/// it would verify the existence of top-level browsing context before sending.
fn top_level_script_command(
&self,
cmd_msg: WebDriverScriptCommand,
verify: VerifyBrowsingContextIsOpen,
) -> WebDriverResult<()> {
let webview_id = self.webview_id()?;
if let VerifyBrowsingContextIsOpen::Yes = verify {
self.verify_top_level_browsing_context_is_open(webview_id)?;
}
let browsing_context_id = BrowsingContextId::from(webview_id);
self.send_message_to_embedder(WebDriverCommandMsg::ScriptCommand(
browsing_context_id,
cmd_msg,
))?;
Ok(())
}
/// <https://w3c.github.io/webdriver/#navigate-to>
fn handle_get(&mut self, parameters: &GetParameters) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 2. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 3. If URL is not an absolute URL or is not an absolute URL with fragment
// or not a local scheme, return error with error code invalid argument.
let url = match ServoUrl::parse(&parameters.url[..]) {
Ok(url) => url,
Err(_) => {
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
"Invalid URL",
));
},
};
// Step 4. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
let cmd_msg =
WebDriverCommandMsg::LoadUrl(webview_id, url, self.load_status_sender.clone());
self.send_message_to_embedder(cmd_msg)?;
// Step 8.2.1: try to wait for navigation to complete.
self.wait_for_navigation_complete()?;
// Step 8.3. Set current browsing context with session and current top browsing context
self.session_mut()?
.set_browsing_context_id(BrowsingContextId::from(webview_id));
Ok(WebDriverResponse::Void)
}
fn wait_document_ready(&self, timeout: u64) -> WebDriverResult<WebDriverResponse> {
select! {
recv(self.load_status_receiver) -> res => {
match res {
// If the navigation is navigation to IFrame, no document state event is fired.
Ok(Ok(WebDriverLoadStatus::Blocked)) => {
// TODO: evaluate the correctness later
// Load status is block means an user prompt is shown.
// Alot of tests expect this to return success
// then the user prompt is handled in the next command.
// If user prompt can't be handler, next command returns
// an error anyway.
Ok(WebDriverResponse::Void)
},
Ok(Ok(WebDriverLoadStatus::Complete)) |
Ok(Ok(WebDriverLoadStatus::NavigationStop)) =>
Ok(WebDriverResponse::Void)
,
_ => Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Unexpected load status received while waiting for document ready state",
)),
}
},
recv(after(Duration::from_millis(timeout))) -> _ => Err(
WebDriverError::new(ErrorStatus::Timeout, "Load timed out")
),
}
}
/// <https://w3c.github.io/webdriver/#dfn-wait-for-navigation-to-complete>
fn wait_for_navigation_complete(&self) -> WebDriverResult<WebDriverResponse> {
debug!("waiting for load");
let session = self.session()?;
// Step 1. If session's page loading strategy is "none",
// return success with data null.
if session.page_loading_strategy() == PageLoadStrategy::None {
return Ok(WebDriverResponse::Void);
}
// Step 2. If session's current browsing context is no longer open,
// return success with data null.
if self
.verify_browsing_context_is_open(self.browsing_context_id()?)
.is_err()
{
return Ok(WebDriverResponse::Void);
}
// Step 3. let timeout be the session's page load timeout.
let timeout = session.session_timeouts().page_load;
// TODO: Step 4. Implement timer parameter
let result = self.wait_document_ready(timeout);
debug!("finished waiting for load with {:?}", result);
result
}
/// <https://w3c.github.io/webdriver/#dfn-wait-for-navigation-to-complete>
fn wait_for_navigation(&self) -> WebDriverResult<WebDriverResponse> {
let navigation_status = match self.load_status_receiver.try_recv() {
Ok(Ok(status)) => status,
// Empty channel means no navigation started. Nothing to wait for.
Err(crossbeam_channel::TryRecvError::Empty) => {
return Ok(WebDriverResponse::Void);
},
Err(crossbeam_channel::TryRecvError::Disconnected) => {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Load status channel disconnected",
));
},
Ok(Err(ipc_error)) => {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
format!("Load status channel ipc error: {ipc_error}"),
));
},
};
match navigation_status {
WebDriverLoadStatus::NavigationStart => self.wait_for_navigation_complete(),
// If the load status is timeout, return an error
WebDriverLoadStatus::Timeout => Err(WebDriverError::new(
ErrorStatus::Timeout,
"Navigation timed out",
)),
// If the load status is blocked, it means a user prompt is shown.
// We should handle the user prompt in the next command.
WebDriverLoadStatus::Blocked => Ok(WebDriverResponse::Void),
WebDriverLoadStatus::NavigationStop | WebDriverLoadStatus::Complete => {
unreachable!("Unexpected load status received")
},
}
}
/// <https://w3c.github.io/webdriver/#dfn-get-current-url>
fn handle_current_url(&self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
let (sender, receiver) = ipc::channel().unwrap();
self.top_level_script_command(
WebDriverScriptCommand::GetUrl(sender),
VerifyBrowsingContextIsOpen::No,
)?;
let url = wait_for_ipc_response(receiver)?;
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(url.as_str())?,
)))
}
/// <https://w3c.github.io/webdriver/#get-window-rect>
fn handle_window_rect(
&self,
verify: VerifyBrowsingContextIsOpen,
) -> WebDriverResult<WebDriverResponse> {
let (sender, receiver) = ipc::channel().unwrap();
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
if let VerifyBrowsingContextIsOpen::Yes = verify {
self.verify_top_level_browsing_context_is_open(webview_id)?;
}
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
self.send_message_to_embedder(WebDriverCommandMsg::GetWindowRect(webview_id, sender))?;
let window_rect = wait_for_ipc_response(receiver)?;
let window_size_response = WindowRectResponse {
x: window_rect.min.x,
y: window_rect.min.y,
width: window_rect.width(),
height: window_rect.height(),
};
Ok(WebDriverResponse::WindowRect(window_size_response))
}
/// <https://w3c.github.io/webdriver/#set-window-rect>
fn handle_set_window_rect(
&self,
params: &WindowRectParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 9 - 10. Input Validation. Already done when deserialize.
// Step 11. In case the Set Window Rect command is partially supported
// (i.e. some combinations of arguments are supported but not others),
// the implmentation is expected to continue with the remaining steps.
// DO NOT return "unsupported operation".
let webview_id = self.webview_id()?;
// Step 12. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 13. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
// (TODO) Step 14. Fully exit fullscreen.
// (TODO) Step 15. Restore the window.
let current = LazyCell::new(|| {
let WebDriverResponse::WindowRect(current) = self
.handle_window_rect(VerifyBrowsingContextIsOpen::No)
.unwrap()
else {
unreachable!("handle_window_size() must return WindowRect");
};
current
});
let (x, y, width, height) = (
params.x.unwrap_or_else(|| current.x),
params.y.unwrap_or_else(|| current.y),
params.width.unwrap_or_else(|| current.width),
params.height.unwrap_or_else(|| current.height),
);
let (sender, receiver) = ipc::channel().unwrap();
// Step 16 - 17. Set the width/height in CSS pixels.
// This should be done as long as one of width/height is not null.
// Step 18 - 19. Set the screen x/y in CSS pixels.
// This should be done as long as one of width/height is not null.
self.send_message_to_embedder(WebDriverCommandMsg::SetWindowRect(
webview_id,
DeviceIndependentIntRect::from_origin_and_size(
Point2D::new(x, y),
Size2D::new(width, height),
),
sender,
))?;
let window_rect = wait_for_ipc_response(receiver)?;
debug!("Result window_rect: {window_rect:?}");
let window_size_response = WindowRectResponse {
x: window_rect.min.x,
y: window_rect.min.y,
width: window_rect.width(),
height: window_rect.height(),
};
Ok(WebDriverResponse::WindowRect(window_size_response))
}
/// <https://w3c.github.io/webdriver/#maximize-window>
fn handle_maximize_window(&mut self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If the remote end does not support the Maximize Window command for session's
// current top-level browsing context for any reason,
// return error with error code unsupported operation.
let webview_id = self.webview_id()?;
// Step 2. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 3. Try to handle any user prompts with session.
self.handle_any_user_prompts(self.webview_id()?)?;
// Step 4. (TODO) Fully exit fullscreen.
// Step 5. (TODO) Restore the window.
// Step 6. Maximize the window of session's current top-level browsing context.
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::MaximizeWebView(webview_id, sender))?;
let window_rect = wait_for_ipc_response(receiver)?;
debug!("Result window_rect: {window_rect:?}");
let window_size_response = WindowRectResponse {
x: window_rect.min.x,
y: window_rect.min.y,
width: window_rect.width(),
height: window_rect.height(),
};
Ok(WebDriverResponse::WindowRect(window_size_response))
}
fn handle_is_enabled(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
let browsing_context = self.browsing_context_id()?;
self.verify_browsing_context_is_open(browsing_context)?;
// Step 2. Try to handle any user prompts with session.
let webview_id = self.webview_id()?;
self.handle_any_user_prompts(webview_id)?;
let (sender, receiver) = ipc::channel().unwrap();
self.browsing_context_script_command(
WebDriverScriptCommand::IsEnabled(element.to_string(), sender),
VerifyBrowsingContextIsOpen::No,
)?;
match wait_for_ipc_response(receiver)? {
Ok(is_enabled) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(is_enabled)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
fn handle_is_selected(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
let browsing_context = self.browsing_context_id()?;
self.verify_browsing_context_is_open(browsing_context)?;
// Step 2. Try to handle any user prompts with session.
let webview_id = self.webview_id()?;
self.handle_any_user_prompts(webview_id)?;
let (sender, receiver) = ipc::channel().unwrap();
self.browsing_context_script_command(
WebDriverScriptCommand::IsSelected(element.to_string(), sender),
VerifyBrowsingContextIsOpen::No,
)?;
match wait_for_ipc_response(receiver)? {
Ok(is_selected) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(is_selected)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#back>
fn handle_go_back(&self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
self.send_message_to_embedder(WebDriverCommandMsg::GoBack(
webview_id,
self.load_status_sender.clone(),
))?;
self.wait_for_navigation_complete()
}
/// <https://w3c.github.io/webdriver/#forward>
fn handle_go_forward(&self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
self.send_message_to_embedder(WebDriverCommandMsg::GoForward(
webview_id,
self.load_status_sender.clone(),
))?;
self.wait_for_navigation_complete()
}
/// <https://w3c.github.io/webdriver/#refresh>
fn handle_refresh(&mut self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
let cmd_msg = WebDriverCommandMsg::Refresh(webview_id, self.load_status_sender.clone());
self.send_message_to_embedder(cmd_msg)?;
// Step 4.1: Try to wait for navigation to complete.
self.wait_for_navigation_complete()?;
// Step 5. Set current browsing context with session and current top browsing context.
self.session_mut()?
.set_browsing_context_id(BrowsingContextId::from(webview_id));
Ok(WebDriverResponse::Void)
}
/// <https://w3c.github.io/webdriver/#get-title>
fn handle_title(&self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
let (sender, receiver) = ipc::channel().unwrap();
self.top_level_script_command(
WebDriverScriptCommand::GetTitle(sender),
VerifyBrowsingContextIsOpen::No,
)?;
// Step 3. Let title be the session's current top-level
// browsing context's active document's title.
let title = wait_for_ipc_response(receiver)?;
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(title)?,
)))
}
/// <https://w3c.github.io/webdriver/#get-window-handle>
fn handle_window_handle(&mut self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Return success with the window handle.
let handle = self
.get_window_handle(webview_id)
.expect("Failed to get window handle of an existing webview");
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(handle)?,
)))
}
/// <https://w3c.github.io/webdriver/#get-window-handles>
fn handle_window_handles(&mut self) -> WebDriverResult<WebDriverResponse> {
let mut handles = self.get_window_handles();
handles.sort_unstable();
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(handles)?,
)))
}
fn get_window_handle(&mut self, webview_id: WebViewId) -> Option<String> {
self.get_window_handles()
.iter()
.find(|id| id == &&webview_id.to_string())
.cloned()
}
fn get_window_handles(&self) -> Vec<String> {
self.get_all_webview_ids()
.into_iter()
.map(|id| id.to_string())
.collect()
}
fn get_all_webview_ids(&self) -> Vec<WebViewId> {
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::GetAllWebViews(sender))
.unwrap();
wait_for_ipc_response(receiver).unwrap_or_default()
}
/// <https://w3c.github.io/webdriver/#find-element>
fn handle_find_element(
&self,
parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 1 - 9.
let res = self.handle_find_elements(parameters)?;
// Step 10. If result is empty, return error with error code no such element.
// Otherwise, return the first element of result.
unwrap_first_element_response(res)
}
/// <https://w3c.github.io/webdriver/#close-window>
fn handle_close_window(&mut self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
// Step 3. Close session's current top-level browsing context.
let (sender, receiver) = ipc::channel().unwrap();
let cmd_msg = WebDriverCommandMsg::CloseWebView(webview_id, sender);
self.send_message_to_embedder(cmd_msg)?;
wait_for_ipc_response(receiver)?;
// Step 4. If there are no more open top-level browsing contexts, try to close the session.
let window_handles = self.get_window_handles();
if window_handles.is_empty() {
self.session = None;
}
// Step 5. Return the result of running the remote end steps for the Get Window Handles command
Ok(WebDriverResponse::CloseWindow(CloseWindowResponse(
window_handles,
)))
}
/// <https://w3c.github.io/webdriver/#new-window>
fn handle_new_window(
&mut self,
_parameters: &NewWindowParameters,
) -> WebDriverResult<WebDriverResponse> {
let (sender, receiver) = ipc::channel().unwrap();
let webview_id = self.webview_id()?;
// Step 2. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 3. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
let cmd_msg =
WebDriverCommandMsg::NewWebView(sender, Some(self.load_status_sender.clone()));
// Step 5. Create a new top-level browsing context by running the window open steps.
// This MUST be done without invoking the focusing steps.
self.send_message_to_embedder(cmd_msg)?;
if let Ok(webview_id) = receiver.recv() {
let _ = self.wait_for_navigation_complete();
let handle = self
.get_window_handle(webview_id)
.expect("Failed to get window handle of an existing webview");
Ok(WebDriverResponse::NewWindow(NewWindowResponse {
handle,
typ: "tab".to_string(),
}))
} else {
Err(WebDriverError::new(
ErrorStatus::UnknownError,
"No webview ID received",
))
}
}
/// <https://w3c.github.io/webdriver/#dfn-switch-to-frame>
fn handle_switch_to_frame(
&mut self,
parameters: &SwitchToFrameParameters,
) -> WebDriverResult<WebDriverResponse> {
use webdriver::common::FrameId;
let frame_id = match parameters.id {
// id is null
FrameId::Top => {
let webview_id = self.webview_id()?;
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Try to handle any user prompts with session.
self.handle_any_user_prompts(webview_id)?;
// Step 3. Set the current browsing context with session and
// session's current top-level browsing context.
let browsing_context_id = BrowsingContextId::from(webview_id);
self.session_mut()?
.set_browsing_context_id(browsing_context_id);
// Step 4. Update any implementation-specific state that would result from
// the user selecting session's current browsing context for interaction,
// without altering OS-level focus.
self.focus_browsing_context(browsing_context_id)?;
return Ok(WebDriverResponse::Void);
},
// id is a Number object
FrameId::Short(ref x) => {
// (Already handled when deserializing in webdriver-crate)
// Step 1. If id is less than 0 or greater than 2^16 1,
// return error with error code invalid argument.
WebDriverFrameId::Short(*x)
},
FrameId::Element(ref x) => WebDriverFrameId::Element(x.to_string()),
};
self.switch_to_frame(frame_id)
}
/// <https://w3c.github.io/webdriver/#switch-to-parent-frame>
fn handle_switch_to_parent_frame(&mut self) -> WebDriverResult<WebDriverResponse> {
let webview_id = self.webview_id()?;
let browsing_context = self.browsing_context_id()?;
// Step 1. If session's current browsing context is already the top-level browsing context:
if browsing_context == webview_id {
// Step 1.1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(browsing_context)?;
// Step 1.2. Return success with data null.
return Ok(WebDriverResponse::Void);
}
// Step 2. If session's current parent browsing context is no longer open,
// return error with error code no such window.
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetParentFrameId(sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::Yes)?;
// Step 3. Handle any user prompt.
self.handle_any_user_prompts(webview_id)?;
// Step 4. If session's current parent browsing context is not null,
// set the current browsing context with session and current parent browsing context.
match wait_for_ipc_response(receiver)? {
Ok(browsing_context_id) => {
self.session_mut()?
.set_browsing_context_id(browsing_context_id);
// Step 5. Update any implementation-specific state that would result from
// the user selecting session's current browsing context for interaction,
// without altering OS-level focus.
self.focus_browsing_context(browsing_context_id)?;
Ok(WebDriverResponse::Void)
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
// https://w3c.github.io/webdriver/#switch-to-window
fn handle_switch_to_window(
&mut self,
parameters: &SwitchToWindowParameters,
) -> WebDriverResult<WebDriverResponse> {
let Some(webview_id) = self
.get_all_webview_ids()
.into_iter()
.find(|id| id.to_string() == parameters.handle)
else {
return Err(WebDriverError::new(
ErrorStatus::NoSuchWindow,
"No such window while switching to window",
));
};
let session = self.session_mut()?;
session.set_webview_id(webview_id);
session.set_browsing_context_id(BrowsingContextId::from(webview_id));
// Step 5. Update any implementation-specific state that would result
// from the user selecting session's current browsing context for interaction,
// without altering OS-level focus.
self.focus_webview(webview_id)?;
Ok(WebDriverResponse::Void)
}
fn switch_to_frame(
&mut self,
frame_id: WebDriverFrameId,
) -> WebDriverResult<WebDriverResponse> {
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetBrowsingContextId(frame_id, sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::Yes)?;
self.handle_any_user_prompts(self.webview_id()?)?;
match wait_for_ipc_response(receiver)? {
Ok(browsing_context_id) => {
self.session_mut()?
.set_browsing_context_id(browsing_context_id);
// Step 4. Update any implementation-specific state that would result from
// the user selecting session's current browsing context for interaction,
// without altering OS-level focus.
self.focus_browsing_context(browsing_context_id)?;
Ok(WebDriverResponse::Void)
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#find-elements>
fn handle_find_elements(
&self,
parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 4. If selector is undefined, return error with error code invalid argument.
if parameters.value.is_empty() {
return Err(WebDriverError::new(ErrorStatus::InvalidArgument, ""));
}
// Step 5. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 6. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
match parameters.using {
LocatorStrategy::CSSSelector => {
let cmd = WebDriverScriptCommand::FindElementsCSSSelector(
parameters.value.clone(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => {
let cmd = WebDriverScriptCommand::FindElementsLinkText(
parameters.value.clone(),
parameters.using == LocatorStrategy::PartialLinkText,
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::TagName => {
let cmd =
WebDriverScriptCommand::FindElementsTagName(parameters.value.clone(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::XPath => {
let cmd = WebDriverScriptCommand::FindElementsXpathSelector(
parameters.value.clone(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
}
match wait_for_ipc_response(receiver)? {
Ok(value) => {
let resp_value: Vec<WebElement> = value.into_iter().map(WebElement).collect();
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(resp_value)?,
)))
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#find-element-from-element>
fn handle_find_element_from_element(
&self,
element: &WebElement,
parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 1 - 8.
let res = self.handle_find_elements_from_element(element, parameters)?;
// Step 9. If result is empty, return error with error code no such element.
// Otherwise, return the first element of result.
unwrap_first_element_response(res)
}
/// <https://w3c.github.io/webdriver/#find-elements-from-element>
fn handle_find_elements_from_element(
&self,
element: &WebElement,
parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 4. If selector is undefined, return error with error code invalid argument.
if parameters.value.is_empty() {
return Err(WebDriverError::new(ErrorStatus::InvalidArgument, ""));
}
// Step 5. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 6. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
match parameters.using {
LocatorStrategy::CSSSelector => {
let cmd = WebDriverScriptCommand::FindElementElementsCSSSelector(
parameters.value.clone(),
element.to_string(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => {
let cmd = WebDriverScriptCommand::FindElementElementsLinkText(
parameters.value.clone(),
element.to_string(),
parameters.using == LocatorStrategy::PartialLinkText,
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::TagName => {
let cmd = WebDriverScriptCommand::FindElementElementsTagName(
parameters.value.clone(),
element.to_string(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::XPath => {
let cmd = WebDriverScriptCommand::FindElementElementsXPathSelector(
parameters.value.clone(),
element.to_string(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
}
match wait_for_ipc_response(receiver)? {
Ok(value) => {
let resp_value: Vec<Value> = value
.into_iter()
.map(|x| serde_json::to_value(WebElement(x)).unwrap())
.collect();
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(resp_value)?,
)))
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#find-elements-from-shadow-root>
fn handle_find_elements_from_shadow_root(
&self,
shadow_root: &ShadowRoot,
parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 4. If selector is undefined, return error with error code invalid argument.
if parameters.value.is_empty() {
return Err(WebDriverError::new(ErrorStatus::InvalidArgument, ""));
}
// Step 5. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 6. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
match parameters.using {
LocatorStrategy::CSSSelector => {
let cmd = WebDriverScriptCommand::FindShadowElementsCSSSelector(
parameters.value.clone(),
shadow_root.to_string(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => {
let cmd = WebDriverScriptCommand::FindShadowElementsLinkText(
parameters.value.clone(),
shadow_root.to_string(),
parameters.using == LocatorStrategy::PartialLinkText,
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::TagName => {
let cmd = WebDriverScriptCommand::FindShadowElementsTagName(
parameters.value.clone(),
shadow_root.to_string(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
LocatorStrategy::XPath => {
let cmd = WebDriverScriptCommand::FindShadowElementsXPathSelector(
parameters.value.clone(),
shadow_root.to_string(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
},
}
match wait_for_ipc_response(receiver)? {
Ok(value) => {
let resp_value: Vec<Value> = value
.into_iter()
.map(|x| serde_json::to_value(WebElement(x)).unwrap())
.collect();
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(resp_value)?,
)))
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#find-element-from-shadow-root>
fn handle_find_element_from_shadow_root(
&self,
shadow_root: &ShadowRoot,
parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 1 - 8.
let res = self.handle_find_elements_from_shadow_root(shadow_root, parameters)?;
// Step 9. If result is empty, return error with error code no such element.
// Otherwise, return the first element of result.
unwrap_first_element_response(res)
}
/// <https://w3c.github.io/webdriver/#get-element-shadow-root>
fn handle_get_shadow_root(&self, element: WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetElementShadowRoot(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(value) => {
let Some(value) = value else {
return Err(WebDriverError::new(ErrorStatus::NoSuchShadowRoot, ""));
};
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(ShadowRoot(value))?,
)))
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#get-element-rect>
fn handle_element_rect(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetElementRect(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(rect) => {
let response = ElementRectResponse {
x: rect.origin.x,
y: rect.origin.y,
width: rect.size.width,
height: rect.size.height,
};
Ok(WebDriverResponse::ElementRect(response))
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#dfn-get-element-text>
fn handle_element_text(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetElementText(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(value)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
///<https://w3c.github.io/webdriver/#get-active-element>
fn handle_active_element(&self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetActiveElement(sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
let value =
wait_for_ipc_response(receiver)?.map(|x| serde_json::to_value(WebElement(x)).unwrap());
// Step 4. If active element is a non-null element, return success
// with data set to web element reference object for session and active element.
// Otherwise, return error with error code no such element.
if value.is_some() {
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(value)?,
)))
} else {
Err(WebDriverError::new(
ErrorStatus::NoSuchElement,
"No active element found",
))
}
}
/// <https://w3c.github.io/webdriver/#get-computed-role>
fn handle_computed_role(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetComputedRole(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(value)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#get-element-tag-name>
fn handle_element_tag_name(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetElementTagName(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(value)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#get-element-attribute>
fn handle_element_attribute(
&self,
element: &WebElement,
name: &str,
) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetElementAttribute(
element.to_string(),
name.to_owned(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(value)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#get-element-property>
fn handle_element_property(
&self,
element: &WebElement,
name: &str,
) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetElementProperty(
element.to_string(),
name.to_owned(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(SendableJSValue(value))?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#get-element-css-value>
fn handle_element_css(
&self,
element: &WebElement,
name: &str,
) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd =
WebDriverScriptCommand::GetElementCSS(element.to_string(), name.to_owned(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(value)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#get-all-cookies>
fn handle_get_cookies(&self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetCookies(sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
let cookies = match wait_for_ipc_response(receiver)? {
Ok(cookies) => cookies,
Err(error) => return Err(WebDriverError::new(error, "")),
};
let response = cookies
.into_iter()
.map(|cookie| cookie_msg_to_cookie(cookie.into_inner()))
.collect::<Vec<Cookie>>();
Ok(WebDriverResponse::Cookies(CookiesResponse(response)))
}
/// <https://w3c.github.io/webdriver/#get-named-cookie>
fn handle_get_cookie(&self, name: String) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetCookie(name, sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
let cookies = match wait_for_ipc_response(receiver)? {
Ok(cookies) => cookies,
Err(error) => return Err(WebDriverError::new(error, "")),
};
let Some(response) = cookies
.into_iter()
.map(|cookie| cookie_msg_to_cookie(cookie.into_inner()))
.next()
else {
return Err(WebDriverError::new(ErrorStatus::NoSuchCookie, ""));
};
Ok(WebDriverResponse::Cookie(CookieResponse(response)))
}
/// <https://w3c.github.io/webdriver/#add-cookie>
fn handle_add_cookie(
&self,
params: &AddCookieParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let mut cookie_builder =
CookieBuilder::new(params.name.to_owned(), params.value.to_owned())
.secure(params.secure)
.http_only(params.httpOnly);
if let Some(ref domain) = params.domain {
cookie_builder = cookie_builder.domain(domain.clone());
}
if let Some(ref path) = params.path {
cookie_builder = cookie_builder.path(path.clone());
}
if let Some(ref expiry) = params.expiry {
if let Ok(datetime) = OffsetDateTime::from_unix_timestamp(expiry.0 as i64) {
cookie_builder = cookie_builder.expires(datetime);
}
}
if let Some(ref same_site) = params.sameSite {
cookie_builder = match same_site.as_str() {
"None" => Ok(cookie_builder.same_site(SameSite::None)),
"Lax" => Ok(cookie_builder.same_site(SameSite::Lax)),
"Strict" => Ok(cookie_builder.same_site(SameSite::Strict)),
_ => Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
"invalid argument",
)),
}?;
}
let cmd = WebDriverScriptCommand::AddCookie(cookie_builder.build(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(_) => Ok(WebDriverResponse::Void),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#delete-cookie>
fn handle_delete_cookie(&self, name: String) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::DeleteCookie(name, sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(_) => Ok(WebDriverResponse::Void),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#delete-all-cookies>
fn handle_delete_cookies(&self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::DeleteCookies(sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::Yes)?;
match wait_for_ipc_response(receiver)? {
Ok(_) => Ok(WebDriverResponse::Void),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
fn handle_get_timeouts(&mut self) -> WebDriverResult<WebDriverResponse> {
let timeouts = self.session()?.session_timeouts();
let timeouts = TimeoutsResponse {
script: timeouts.script,
page_load: timeouts.page_load,
implicit: timeouts.implicit_wait,
};
Ok(WebDriverResponse::Timeouts(timeouts))
}
fn handle_set_timeouts(
&mut self,
parameters: &TimeoutsParameters,
) -> WebDriverResult<WebDriverResponse> {
let session = self.session_mut()?;
if let Some(timeout) = parameters.script {
session.session_timeouts_mut().script = timeout;
}
if let Some(timeout) = parameters.page_load {
session.session_timeouts_mut().page_load = timeout;
}
if let Some(timeout) = parameters.implicit {
session.session_timeouts_mut().implicit_wait = timeout
}
Ok(WebDriverResponse::Void)
}
/// <https://w3c.github.io/webdriver/#dfn-get-page-source>
fn handle_get_page_source(&self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::GetPageSource(sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(source) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(source)?,
))),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#perform-actions>
fn handle_perform_actions(
&mut self,
parameters: ActionsParameters,
) -> WebDriverResult<WebDriverResponse> {
let browsing_context = self.browsing_context_id()?;
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(browsing_context)?;
// Step 2. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
// Step 5. Let actions by tick be the result of trying to extract an action sequence
let actions_by_tick = self.extract_an_action_sequence(parameters);
// Step 6. Dispatch actions with current browsing context
match self.dispatch_actions(actions_by_tick, browsing_context) {
Ok(_) => Ok(WebDriverResponse::Void),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#dfn-release-actions>
fn handle_release_actions(&mut self) -> WebDriverResult<WebDriverResponse> {
let session = self.session()?;
let browsing_context_id = self.browsing_context_id()?;
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(browsing_context_id)?;
// Step 2. User prompts. No user prompt implemented yet.
self.handle_any_user_prompts(self.webview_id()?)?;
// Skip: Step 3. We don't support "browsing context input state map" yet.
// TODO: Step 4. Actions options are not used yet.
// Step 5. Not needed because "In a session that is only a HTTP session
// only one command can run at a time, so this will never block."
// Step 6. Let undo actions be input cancel list in reverse order.
let undo_actions = session
.input_cancel_list_mut()
.drain(..)
.rev()
.map(|(id, action_item)| Vec::from([(id, action_item)]))
.collect();
// Step 7. Dispatch undo actions with current browsing context.
if let Err(err) = self.dispatch_actions(undo_actions, browsing_context_id) {
return Err(WebDriverError::new(err, "Failed to dispatch undo actions"));
}
// Step 8. Reset the input state of session's current top-level browsing context.
session.input_state_table_mut().clear();
Ok(WebDriverResponse::Void)
}
/// <https://w3c.github.io/webdriver/#dfn-execute-script>
fn handle_execute_script(
&self,
parameters: JavascriptCommandParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 1. Let body and arguments be the result of trying to extract the script arguments
// from a request with argument parameters.
let (func_body, args_string) = self.extract_script_arguments(parameters)?;
// This is pretty ugly; we really want something that acts like
// new Function() and then takes the resulting function and executes
// it with a vec of arguments.
let script = format!(
"(function() {{ {}\n }})({})",
func_body,
args_string.join(", ")
);
debug!("{}", script);
// Step 2. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 3. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::ExecuteScript(script, sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
let result = wait_for_ipc_response(receiver)?;
self.postprocess_js_result(result)
}
fn handle_execute_async_script(
&self,
parameters: JavascriptCommandParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 1. Let body and arguments be the result of trying to extract the script arguments
// from a request with argument parameters.
let (func_body, mut args_string) = self.extract_script_arguments(parameters)?;
args_string.push("resolve".to_string());
let timeout_script = if let Some(script_timeout) = self.session()?.session_timeouts().script
{
format!("setTimeout(webdriverTimeout, {});", script_timeout)
} else {
"".into()
};
let script = format!(
r#"(function() {{
let webdriverPromise = new Promise(function(resolve, reject) {{
{}
(async function() {{
{}
}})({})
.then((v) => {{}}, (err) => reject(err))
}})
.then((v) => window.webdriverCallback(v), (r) => window.webdriverException(r))
.catch((r) => window.webdriverException(r));
}})();"#,
timeout_script,
func_body,
args_string.join(", "),
);
debug!("{}", script);
// Step 2. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 3. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::ExecuteAsyncScript(script, sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
let result = wait_for_ipc_response(receiver)?;
self.postprocess_js_result(result)
}
fn postprocess_js_result(
&self,
result: WebDriverJSResult,
) -> WebDriverResult<WebDriverResponse> {
match result {
Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(SendableJSValue(value))?,
))),
Err(WebDriverJSError::BrowsingContextNotFound) => Err(WebDriverError::new(
ErrorStatus::NoSuchWindow,
"Pipeline id not found in browsing context",
)),
Err(WebDriverJSError::JSException(_e)) => Err(WebDriverError::new(
ErrorStatus::JavascriptError,
"JS evaluation raised an exception",
)),
Err(WebDriverJSError::JSError) => Err(WebDriverError::new(
ErrorStatus::JavascriptError,
"JS evaluation raised an unknown exception",
)),
Err(WebDriverJSError::StaleElementReference) => Err(WebDriverError::new(
ErrorStatus::StaleElementReference,
"Stale element",
)),
Err(WebDriverJSError::DetachedShadowRoot) => Err(WebDriverError::new(
ErrorStatus::DetachedShadowRoot,
"Detached shadow root",
)),
Err(WebDriverJSError::Timeout) => {
Err(WebDriverError::new(ErrorStatus::ScriptTimeout, ""))
},
Err(WebDriverJSError::UnknownType) => Err(WebDriverError::new(
ErrorStatus::UnsupportedOperation,
"Unsupported return type",
)),
}
}
/// <https://w3c.github.io/webdriver/#dfn-element-send-keys>
fn handle_element_send_keys(
&mut self,
element: &WebElement,
keys: &SendKeysParameters,
) -> WebDriverResult<WebDriverResponse> {
// Step 3. If session's current browsing context is no longer open,
// return error with error code no such window.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 4. Handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::WillSendKeys(
element.to_string(),
keys.text.to_string(),
self.session()?.strict_file_interactability(),
sender,
);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
// File input and non-typeable form control should have
// been handled in `webdriver_handler.rs`.
if !wait_for_ipc_response(receiver)?.map_err(|error| WebDriverError::new(error, ""))? {
return Ok(WebDriverResponse::Void);
}
// Step 10. Let input id be a the result of generating a UUID.
let id = Uuid::new_v4().to_string();
// Step 12. Add an input source
self.session_mut()?
.input_state_table_mut()
.insert(id.clone(), InputSourceState::Key(KeyInputState::new()));
// Step 13. dispatch actions for a string
// https://w3c.github.io/webdriver/#dfn-dispatch-actions-for-a-string
let input_events = send_keys(&keys.text);
for event in input_events {
match event {
DispatchStringEvent::Keyboard(event) => {
let raw_string = convert_keyboard_event_to_string(&event);
let key_action = match event.state {
KeyState::Down => KeyAction::Down(KeyDownAction { value: raw_string }),
KeyState::Up => KeyAction::Up(KeyUpAction { value: raw_string }),
};
let action_sequence = ActionSequence {
id: id.clone(),
actions: ActionsType::Key {
actions: vec![KeyActionItem::Key(key_action)],
},
};
let actions_by_tick = self.actions_by_tick_from_sequence(vec![action_sequence]);
if let Err(e) =
self.dispatch_actions(actions_by_tick, self.browsing_context_id()?)
{
log::error!("handle_element_send_keys: dispatch_actions failed: {:?}", e);
}
},
DispatchStringEvent::Composition(event) => {
let cmd_msg =
WebDriverCommandMsg::DispatchComposition(self.webview_id()?, event);
self.send_message_to_embedder(cmd_msg)?;
},
}
}
// Step 14. Remove an input source with input state and input id.
self.session_mut()?.input_state_table_mut().remove(&id);
Ok(WebDriverResponse::Void)
}
/// <https://w3c.github.io/webdriver/#element-clear>
fn handle_element_clear(&self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return ErrorStatus::NoSuchWindow.
self.verify_browsing_context_is_open(self.browsing_context_id()?)?;
// Step 2. Try to handle any user prompt.
self.handle_any_user_prompts(self.webview_id()?)?;
// Step 3-11 handled in script thread.
let (sender, receiver) = ipc::channel().unwrap();
let cmd = WebDriverScriptCommand::ElementClear(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(_) => Ok(WebDriverResponse::Void),
Err(error) => Err(WebDriverError::new(error, "")),
}
}
/// <https://w3c.github.io/webdriver/#element-click>
fn handle_element_click(&mut self, element: &WebElement) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current browsing context is no longer open,
// return error with error code no such window.
let browsing_context_id = self.browsing_context_id()?;
self.verify_browsing_context_is_open(browsing_context_id)?;
// Step 2. Handle any user prompts.
self.handle_any_user_prompts(self.webview_id()?)?;
let (sender, receiver) = ipc::channel().unwrap();
// Steps 3-7 + Step 8 for <option> are handled in script thread.
let cmd = WebDriverScriptCommand::ElementClick(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
match wait_for_ipc_response(receiver)? {
Ok(element_id) => match element_id {
Some(element_id) => {
// Load status sender should be set up before we dispatch actions
// to ensure webdriver can capture any navigation events.
self.add_load_status_sender()?;
self.perform_element_click(element_id)?;
// Step 11. Try to wait for navigation to complete with session.
// Check if there is a navigation with script
let res = self.wait_for_navigation()?;
// Clear the load status sender
self.clear_load_status_sender()?;
Ok(res)
},
// Step 13
None => Ok(WebDriverResponse::Void),
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
fn perform_element_click(&mut self, element: String) -> WebDriverResult<WebDriverResponse> {
// Step 8 for elements other than <option>
let id = Uuid::new_v4().to_string();
// Step 8.1
self.session_mut()?.input_state_table_mut().insert(
id.clone(),
InputSourceState::Pointer(PointerInputState::new(PointerType::Mouse)),
);
// Step 8.7. Construct a pointer move action.
// Step 8.8. Set a property x to 0 on pointer move action.
// Step 8.9. Set a property y to 0 on pointer move action.
// Step 8.10. Set a property origin to element on pointer move action.
let pointer_move_action = PointerMoveAction {
duration: None,
origin: PointerOrigin::Element(WebElement(element)),
x: 0.0,
y: 0.0,
..Default::default()
};
// Step 8.11. Construct pointer down action.
// Step 8.12. Set a property button to 0 on pointer down action.
let pointer_down_action = PointerDownAction {
button: i16::from(MouseButton::Left) as u64,
..Default::default()
};
// Step 8.13. Construct pointer up action.
// Step 8.14. Set a property button to 0 on pointer up action.
let pointer_up_action = PointerUpAction {
button: i16::from(MouseButton::Left) as u64,
..Default::default()
};
let action_sequence = ActionSequence {
id: id.clone(),
actions: ActionsType::Pointer {
parameters: PointerActionParameters {
pointer_type: PointerType::Mouse,
},
actions: vec![
PointerActionItem::Pointer(PointerAction::Move(pointer_move_action)),
PointerActionItem::Pointer(PointerAction::Down(pointer_down_action)),
PointerActionItem::Pointer(PointerAction::Up(pointer_up_action)),
],
},
};
// Step 8.16. Dispatch a list of actions with session's current browsing context
let actions_by_tick = self.actions_by_tick_from_sequence(vec![action_sequence]);
if let Err(e) = self.dispatch_actions(actions_by_tick, self.browsing_context_id()?) {
log::error!("handle_element_click: dispatch_actions failed: {:?}", e);
}
// Step 8.17 Remove an input source with input state and input id.
self.session_mut()?.input_state_table_mut().remove(&id);
Ok(WebDriverResponse::Void)
}
fn take_screenshot(&self, rect: Option<Rect<f32, CSSPixel>>) -> WebDriverResult<String> {
// Spec: Take screenshot after running the animation frame callbacks.
let _ = self.handle_execute_async_script(JavascriptCommandParameters {
script: "requestAnimationFrame(() => arguments[0]());".to_string(),
args: None,
});
let webview_id = self.webview_id()?;
let mut img = None;
let interval = 1000;
let iterations = 30000 / interval;
for _ in 0..iterations {
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::TakeScreenshot(
webview_id, rect, sender,
))?;
match wait_for_ipc_response(receiver)? {
Ok(output_img) => {
if let Some(x) = output_img {
img = Some(x);
break;
}
},
Err(()) => {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
"The bounding box of element has either 0 width or 0 height",
));
},
};
thread::sleep(Duration::from_millis(interval));
}
let img = match img {
Some(img) => img,
None => {
return Err(WebDriverError::new(
ErrorStatus::Timeout,
"Taking screenshot timed out",
));
},
};
// The compositor always sends RGBA pixels.
assert_eq!(
img.format,
PixelFormat::RGBA8,
"Unexpected screenshot pixel format"
);
let rgb = RgbaImage::from_raw(
img.metadata.width,
img.metadata.height,
img.first_frame().bytes.to_vec(),
)
.unwrap();
let mut png_data = Cursor::new(Vec::new());
DynamicImage::ImageRgba8(rgb)
.write_to(&mut png_data, ImageFormat::Png)
.unwrap();
Ok(base64::engine::general_purpose::STANDARD.encode(png_data.get_ref()))
}
fn handle_take_screenshot(&self) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
let webview_id = self.webview_id()?;
self.verify_top_level_browsing_context_is_open(webview_id)?;
self.handle_any_user_prompts(webview_id)?;
// Step 2
let encoded = self.take_screenshot(None)?;
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(encoded)?,
)))
}
fn handle_take_element_screenshot(
&self,
element: &WebElement,
) -> WebDriverResult<WebDriverResponse> {
// Step 1. If session's current top-level browsing context is no longer open,
// return error with error code no such window.
let webview_id = self.webview_id()?;
self.verify_top_level_browsing_context_is_open(webview_id)?;
// Step 2. Try to handle any user prompts with session.
self.handle_any_user_prompts(webview_id)?;
// Step 3. Trying to get element.
// Step 4. Scroll into view into element.
let (sender, receiver) = ipc::channel().unwrap();
let cmd =
WebDriverScriptCommand::ScrollAndGetBoundingClientRect(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::Yes)?;
match wait_for_ipc_response(receiver)? {
Ok(rect) => {
// Step 5
let encoded = self.take_screenshot(Some(Rect::from_untyped(&rect)))?;
// Step 6 return success with data encoded string.
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(encoded)?,
)))
},
Err(error) => Err(WebDriverError::new(error, "")),
}
}
fn handle_get_prefs(
&self,
parameters: &GetPrefsParameters,
) -> WebDriverResult<WebDriverResponse> {
let prefs = parameters
.prefs
.iter()
.map(|item| {
(
item.clone(),
serde_json::to_value(prefs::get().get_value(item)).unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(prefs)?,
)))
}
fn handle_set_prefs(
&self,
parameters: &SetPrefsParameters,
) -> WebDriverResult<WebDriverResponse> {
let mut current_preferences = prefs::get().clone();
for (key, value) in parameters.prefs.iter() {
current_preferences.set_value(key, value.0.clone());
}
prefs::set(current_preferences);
Ok(WebDriverResponse::Void)
}
fn handle_reset_prefs(
&self,
parameters: &GetPrefsParameters,
) -> WebDriverResult<WebDriverResponse> {
let (new_preferences, map) = if parameters.prefs.is_empty() {
(Preferences::default(), BTreeMap::new())
} else {
// If we only want to reset some of the preferences.
let mut new_preferences = prefs::get().clone();
let default_preferences = Preferences::default();
for key in parameters.prefs.iter() {
new_preferences.set_value(key, default_preferences.get_value(key))
}
let map = parameters
.prefs
.iter()
.map(|item| (item.clone(), new_preferences.get_value(item)))
.collect::<BTreeMap<_, _>>();
(new_preferences, map)
};
prefs::set(new_preferences);
Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(map)?,
)))
}
fn verify_top_level_browsing_context_is_open(
&self,
webview_id: WebViewId,
) -> Result<(), WebDriverError> {
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::IsWebViewOpen(webview_id, sender))?;
if !receiver.recv().unwrap_or(false) {
Err(WebDriverError::new(
ErrorStatus::NoSuchWindow,
"No such window",
))
} else {
Ok(())
}
}
fn verify_browsing_context_is_open(
&self,
browsing_context_id: BrowsingContextId,
) -> Result<(), WebDriverError> {
let (sender, receiver) = ipc::channel().unwrap();
self.send_message_to_embedder(WebDriverCommandMsg::IsBrowsingContextOpen(
browsing_context_id,
sender,
))?;
if !receiver.recv().unwrap_or(false) {
Err(WebDriverError::new(
ErrorStatus::NoSuchWindow,
"No such window",
))
} else {
Ok(())
}
}
fn focus_webview(&self, webview_id: WebViewId) -> WebDriverResult<()> {
self.send_message_to_embedder(WebDriverCommandMsg::FocusWebView(webview_id))
}
fn focus_browsing_context(&self, browsing_cotext_id: BrowsingContextId) -> WebDriverResult<()> {
self.send_message_to_embedder(WebDriverCommandMsg::FocusBrowsingContext(
browsing_cotext_id,
))
}
}
impl WebDriverHandler<ServoExtensionRoute> for Handler {
fn handle_command(
&mut self,
_session: &Option<Session>,
msg: WebDriverMessage<ServoExtensionRoute>,
) -> WebDriverResult<WebDriverResponse> {
info!("{:?}", msg.command);
// Drain the load status receiver to avoid incorrect status handling
while self.load_status_receiver.try_recv().is_ok() {}
// Unless we are trying to create a new session, we need to ensure that a
// session has previously been created
match msg.command {
WebDriverCommand::NewSession(_) |
WebDriverCommand::Status |
WebDriverCommand::DeleteSession => {},
_ => {
self.session()?;
},
}
match msg.command {
WebDriverCommand::NewSession(ref parameters) => self.handle_new_session(parameters),
WebDriverCommand::DeleteSession => self.handle_delete_session(),
WebDriverCommand::Status => self.handle_status(),
WebDriverCommand::AddCookie(ref parameters) => self.handle_add_cookie(parameters),
WebDriverCommand::Get(ref parameters) => self.handle_get(parameters),
WebDriverCommand::GetCurrentUrl => self.handle_current_url(),
WebDriverCommand::GetWindowRect => {
self.handle_window_rect(VerifyBrowsingContextIsOpen::Yes)
},
WebDriverCommand::SetWindowRect(ref size) => self.handle_set_window_rect(size),
WebDriverCommand::IsEnabled(ref element) => self.handle_is_enabled(element),
WebDriverCommand::IsSelected(ref element) => self.handle_is_selected(element),
WebDriverCommand::GoBack => self.handle_go_back(),
WebDriverCommand::GoForward => self.handle_go_forward(),
WebDriverCommand::Refresh => self.handle_refresh(),
WebDriverCommand::GetTitle => self.handle_title(),
WebDriverCommand::GetWindowHandle => self.handle_window_handle(),
WebDriverCommand::GetWindowHandles => self.handle_window_handles(),
WebDriverCommand::NewWindow(ref parameters) => self.handle_new_window(parameters),
WebDriverCommand::CloseWindow => self.handle_close_window(),
WebDriverCommand::MaximizeWindow => self.handle_maximize_window(),
WebDriverCommand::SwitchToFrame(ref parameters) => {
self.handle_switch_to_frame(parameters)
},
WebDriverCommand::SwitchToParentFrame => self.handle_switch_to_parent_frame(),
WebDriverCommand::SwitchToWindow(ref parameters) => {
self.handle_switch_to_window(parameters)
},
WebDriverCommand::FindElement(ref parameters) => self.handle_find_element(parameters),
WebDriverCommand::FindElements(ref parameters) => self.handle_find_elements(parameters),
WebDriverCommand::FindElementElement(ref element, ref parameters) => {
self.handle_find_element_from_element(element, parameters)
},
WebDriverCommand::FindElementElements(ref element, ref parameters) => {
self.handle_find_elements_from_element(element, parameters)
},
WebDriverCommand::FindShadowRootElements(ref shadow_root, ref parameters) => {
self.handle_find_elements_from_shadow_root(shadow_root, parameters)
},
WebDriverCommand::FindShadowRootElement(ref shadow_root, ref parameters) => {
self.handle_find_element_from_shadow_root(shadow_root, parameters)
},
WebDriverCommand::GetShadowRoot(element) => self.handle_get_shadow_root(element),
WebDriverCommand::GetNamedCookie(name) => self.handle_get_cookie(name),
WebDriverCommand::GetCookies => self.handle_get_cookies(),
WebDriverCommand::GetActiveElement => self.handle_active_element(),
WebDriverCommand::GetComputedRole(ref element) => self.handle_computed_role(element),
WebDriverCommand::GetElementRect(ref element) => self.handle_element_rect(element),
WebDriverCommand::GetElementText(ref element) => self.handle_element_text(element),
WebDriverCommand::GetElementTagName(ref element) => {
self.handle_element_tag_name(element)
},
WebDriverCommand::GetElementAttribute(ref element, ref name) => {
self.handle_element_attribute(element, name)
},
WebDriverCommand::GetElementProperty(ref element, ref name) => {
self.handle_element_property(element, name)
},
WebDriverCommand::GetCSSValue(ref element, ref name) => {
self.handle_element_css(element, name)
},
WebDriverCommand::GetPageSource => self.handle_get_page_source(),
WebDriverCommand::PerformActions(actions_parameters) => {
self.handle_perform_actions(actions_parameters)
},
WebDriverCommand::ReleaseActions => self.handle_release_actions(),
WebDriverCommand::ExecuteScript(x) => self.handle_execute_script(x),
WebDriverCommand::ExecuteAsyncScript(x) => self.handle_execute_async_script(x),
WebDriverCommand::ElementSendKeys(ref element, ref keys) => {
self.handle_element_send_keys(element, keys)
},
WebDriverCommand::ElementClear(ref element) => self.handle_element_clear(element),
WebDriverCommand::ElementClick(ref element) => self.handle_element_click(element),
WebDriverCommand::DismissAlert => self.handle_dismiss_alert(),
WebDriverCommand::AcceptAlert => self.handle_accept_alert(),
WebDriverCommand::GetAlertText => self.handle_get_alert_text(),
WebDriverCommand::SendAlertText(text) => self.handle_send_alert_text(text.text),
WebDriverCommand::DeleteCookies => self.handle_delete_cookies(),
WebDriverCommand::DeleteCookie(name) => self.handle_delete_cookie(name),
WebDriverCommand::GetTimeouts => self.handle_get_timeouts(),
WebDriverCommand::SetTimeouts(ref x) => self.handle_set_timeouts(x),
WebDriverCommand::TakeScreenshot => self.handle_take_screenshot(),
WebDriverCommand::TakeElementScreenshot(ref x) => {
self.handle_take_element_screenshot(x)
},
WebDriverCommand::Extension(ref extension) => match *extension {
ServoExtensionCommand::GetPrefs(ref x) => self.handle_get_prefs(x),
ServoExtensionCommand::SetPrefs(ref x) => self.handle_set_prefs(x),
ServoExtensionCommand::ResetPrefs(ref x) => self.handle_reset_prefs(x),
},
_ => Err(WebDriverError::new(
ErrorStatus::UnsupportedOperation,
format!("Command not implemented: {:?}", msg.command),
)),
}
}
fn teardown_session(&mut self, _session: SessionTeardownKind) {
self.session = None;
}
}
fn wait_for_ipc_response<T>(receiver: IpcReceiver<T>) -> Result<T, WebDriverError>
where
T: for<'de> Deserialize<'de> + Serialize,
{
receiver
.recv()
.map_err(|_| WebDriverError::new(ErrorStatus::NoSuchWindow, ""))
}
fn unwrap_first_element_response(res: WebDriverResponse) -> WebDriverResult<WebDriverResponse> {
if let WebDriverResponse::Generic(ValueResponse(values)) = res {
let arr = values.as_array().unwrap();
if let Some(first) = arr.first() {
Ok(WebDriverResponse::Generic(ValueResponse(first.clone())))
} else {
Err(WebDriverError::new(ErrorStatus::NoSuchElement, ""))
}
} else {
unreachable!()
}
}
fn convert_keyboard_event_to_string(event: &KeyboardEvent) -> String {
let key = &event.key;
let named_key = match key {
Key::Character(s) => return s.to_string(),
Key::Named(named_key) => named_key,
};
match event.location {
Location::Left | Location::Standard => match named_key {
NamedKey::Unidentified => '\u{E000}'.to_string(),
NamedKey::Cancel => '\u{E001}'.to_string(),
NamedKey::Help => '\u{E002}'.to_string(),
NamedKey::Backspace => '\u{E003}'.to_string(),
NamedKey::Tab => '\u{E004}'.to_string(),
NamedKey::Clear => '\u{E005}'.to_string(),
NamedKey::Enter => match event.code {
Code::NumpadEnter => '\u{E007}'.to_string(),
_ => '\u{E006}'.to_string(),
},
NamedKey::Shift => '\u{E008}'.to_string(),
NamedKey::Control => '\u{E009}'.to_string(),
NamedKey::Alt => '\u{E00A}'.to_string(),
NamedKey::Pause => '\u{E00B}'.to_string(),
NamedKey::Escape => '\u{E00C}'.to_string(),
NamedKey::PageUp => '\u{E00E}'.to_string(),
NamedKey::PageDown => '\u{E00F}'.to_string(),
NamedKey::End => '\u{E010}'.to_string(),
NamedKey::Home => '\u{E011}'.to_string(),
NamedKey::ArrowLeft => '\u{E012}'.to_string(),
NamedKey::ArrowUp => '\u{E013}'.to_string(),
NamedKey::ArrowRight => '\u{E014}'.to_string(),
NamedKey::ArrowDown => '\u{E015}'.to_string(),
NamedKey::Insert => '\u{E016}'.to_string(),
NamedKey::Delete => '\u{E017}'.to_string(),
NamedKey::F1 => '\u{E031}'.to_string(),
NamedKey::F2 => '\u{E032}'.to_string(),
NamedKey::F3 => '\u{E033}'.to_string(),
NamedKey::F4 => '\u{E034}'.to_string(),
NamedKey::F5 => '\u{E035}'.to_string(),
NamedKey::F6 => '\u{E036}'.to_string(),
NamedKey::F7 => '\u{E037}'.to_string(),
NamedKey::F8 => '\u{E038}'.to_string(),
NamedKey::F9 => '\u{E039}'.to_string(),
NamedKey::F10 => '\u{E03A}'.to_string(),
NamedKey::F11 => '\u{E03B}'.to_string(),
NamedKey::F12 => '\u{E03C}'.to_string(),
NamedKey::Meta => '\u{E03D}'.to_string(),
NamedKey::ZenkakuHankaku => '\u{E040}'.to_string(),
_ => {
error!("Unexpected NamedKey on send_keys");
'\u{E000}'.to_string()
},
},
Location::Right | Location::Numpad => match named_key {
NamedKey::Shift => '\u{E050}'.to_string(),
NamedKey::Control => '\u{E051}'.to_string(),
NamedKey::Alt => '\u{E052}'.to_string(),
NamedKey::Meta => '\u{E053}'.to_string(),
NamedKey::PageUp => '\u{E054}'.to_string(),
NamedKey::PageDown => '\u{E055}'.to_string(),
NamedKey::End => '\u{E056}'.to_string(),
NamedKey::Home => '\u{E057}'.to_string(),
NamedKey::ArrowLeft => '\u{E058}'.to_string(),
NamedKey::ArrowUp => '\u{E059}'.to_string(),
NamedKey::ArrowRight => '\u{E05A}'.to_string(),
NamedKey::ArrowDown => '\u{E05B}'.to_string(),
NamedKey::Insert => '\u{E05C}'.to_string(),
NamedKey::Delete => '\u{E05D}'.to_string(),
_ => {
error!("Unexpected NamedKey on send_keys");
'\u{E000}'.to_string()
},
},
}
}