/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/webdriver_server/lib.rs
2 970 строк
123 KB
Euclid Ye
webdriver: Fix race condition in asynchronous script execution (#45374)
04 июн 2026, 14:22
Не верифицирован
04 июн 2026, 14:22
16715c3
Код
Авторство
О чём код?
/* 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 script_argument_extraction; mod server; mod session; mod timeout; mod user_prompt; use std::borrow::ToOwned; use std::cell::LazyCell; use std::collections::{BTreeMap, HashMap}; use std::io::Cursor; use std::net::{SocketAddr, SocketAddrV4}; use std::thread::sleep; use std::time::{Duration, Instant}; use std::{env, fmt, process, thread}; use base64::Engine; use capabilities::ServoCapabilities; use cookie::{CookieBuilder, Expiration, SameSite}; use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, after, select, unbounded}; use embedder_traits::{ CustomHandlersAutomationMode, EventLoopWaker, ImeEvent, InputEvent, JSValue, JavaScriptEvaluationError, JavaScriptEvaluationResultSerializationError, NewWindowTypeHint, WebDriverCommandMsg, WebDriverFrameId, WebDriverJSResult, WebDriverLoadStatus, WebDriverScriptCommand, }; use euclid::{Point2D, Rect, Size2D}; use http::method::Method; use image::{DynamicImage, ImageFormat}; 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 serde::de::{Deserializer, MapAccess, Visitor}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use server::{Session, SessionTeardownKind, WebDriverHandler}; use servo_base::generic_channel::{self, GenericReceiver, GenericSender, RoutedReceiver}; use servo_base::id::{BrowsingContextId, WebViewId}; 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 crate::actions::{ELEMENT_CLICK_BUTTON, InputSourceState, PendingActions, PointerInputState}; use crate::session::{PageLoadStrategy, WebDriverSession}; use crate::timeout::{DEFAULT_IMPLICIT_WAIT, DEFAULT_PAGE_LOAD_TIMEOUT, SCREENSHOT_TIMEOUT}; /// <https://262.ecma-international.org/6.0/#sec-number.max_safe_integer> /// 2^53 - 1 pub(crate) static MAXIMUM_SAFE_INTEGER: u64 = 9_007_199_254_740_991; fn extension_routes() -> Vec<(Method, &'static str, ServoExtensionRoute)> { vec![ ( Method::GET, "/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, ), ( Method::DELETE, "/session/{sessionId}/servo/shutdown", ServoExtensionRoute::Shutdown, ), // <https://html.spec.whatwg.org/multipage/#set-rph-registration-mode> ( Method::POST, "/session/{sessionId}/custom-handlers/set-mode", ServoExtensionRoute::CustomHandlersSetMode, ), ( Method::POST, "/session/{sessionId}/servo/cookies/reset", ServoExtensionRoute::ResetAllCookies, ), ] } 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>, default_preferences: Preferences, ) { let handler = Handler::new(embedder_sender, event_loop_waker, default_preferences); 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(e) => panic!("Unable to start WebDriver HTTP server {e:?}"), } }) .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>, /// A list of [`Receiver`]s that are used to track when input events are handled in the DOM. /// Once these receivers receive a response, we know that the event has been handled. /// /// TODO: Once we upgrade crossbeam-channel this can be replaced with a `WaitGroup`. pending_input_event_receivers: Vec<Receiver<()>>, /// Ongoing [`PointerMoveAction`] or [`WheelScrollAction`] that are being incrementally /// processed across multiple execution cycles within the current tick. pending_actions: Vec<PendingActions>, /// The base set of preferences to treat as default when resetting. default_preferences: Preferences, } #[derive(Clone, Copy, Debug, PartialEq)] enum ServoExtensionRoute { GetPrefs, SetPrefs, ResetPrefs, /// TODO: Shutdown does not actually use sessionId. /// But the webdriver crate always checks existence of sessionID /// except for WebDriverCommand::Status. /// We have to either use our own fork, or relies on the current workaround: /// passing any dummy sessionID. Shutdown, CustomHandlersSetMode, ResetAllCookies, } 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) }, ServoExtensionRoute::CustomHandlersSetMode => { let parameters: CustomHandlersSetModeParameters = serde_json::from_value(body_data.clone())?; ServoExtensionCommand::CustomHandlersSetMode(parameters) }, ServoExtensionRoute::Shutdown => ServoExtensionCommand::Shutdown, ServoExtensionRoute::ResetAllCookies => ServoExtensionCommand::ResetAllCookies, }; Ok(WebDriverCommand::Extension(command)) } } #[derive(Clone, Debug)] enum ServoExtensionCommand { GetPrefs(GetPrefsParameters), SetPrefs(SetPrefsParameters), ResetPrefs(GetPrefsParameters), CustomHandlersSetMode(CustomHandlersSetModeParameters), Shutdown, ResetAllCookies, } 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(), ServoExtensionCommand::CustomHandlersSetMode(ref x) => serde_json::to_value(x).ok(), ServoExtensionCommand::Shutdown | ServoExtensionCommand::ResetAllCookies => None, } } } #[derive(Clone)] struct SendableJSValue(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(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), PrefValue::UInt(u) => serializer.serialize_u64(u), } } } 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)>, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] struct CustomHandlersSetModeParameters { mode: String, } 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, } enum ImplicitWait { Return, #[expect(dead_code, reason = "This will be used in the next patch")] Continue, } impl From<ImplicitWait> for bool { fn from(implicit_wait: ImplicitWait) -> Self { match implicit_wait { ImplicitWait::Return => true, ImplicitWait::Continue => false, } } } impl Handler { fn new( embedder_sender: Sender<WebDriverCommandMsg>, event_loop_waker: Box<dyn EventLoopWaker>, default_preferences: Preferences, ) -> 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, default_preferences, pending_input_event_receivers: Default::default(), pending_actions: Default::default(), } } fn browsing_context_id(&self) -> WebDriverResult<BrowsingContextId> { self.session()? .current_browsing_context_id() .ok_or_else(|| { WebDriverError::new(ErrorStatus::UnknownError, "No browsing context available") }) } fn webview_id(&self) -> WebDriverResult<WebViewId> { self.session()? .current_webview_id() .ok_or_else(|| WebDriverError::new(ErrorStatus::UnknownError, "No webview available")) } // FIXME: This should be completely removed after we revamp the touch chain. fn send_input_event_to_embedder(&self, input_event: InputEvent) { let _ = self.send_message_to_embedder(WebDriverCommandMsg::InputEvent( self.verified_webview_id(), input_event, None, )); } fn send_blocking_input_event_to_embedder(&mut self, input_event: InputEvent) { let (result_sender, result_receiver) = unbounded(); if self .send_message_to_embedder(WebDriverCommandMsg::InputEvent( self.verified_webview_id(), input_event, Some(result_sender), )) .is_ok() { self.pending_input_event_receivers.push(result_receiver); } } 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) = generic_channel::oneshot().unwrap(); self.send_message_to_embedder(WebDriverCommandMsg::GetFocusedWebView(sender))?; // Wait until the document is ready before returning the top-level browsing context id. wait_for_oneshot_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.wait_until_browsing_context_is_open(BrowsingContextId::from(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) = generic_channel::oneshot().unwrap(); self.send_message_to_embedder(WebDriverCommandMsg::NewWindow( NewWindowTypeHint::Auto, 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.wait_until_browsing_context_is_open(BrowsingContextId::from(webview_id))?; self.session_mut()? .set_browsing_context_id(BrowsingContextId::from(webview_id)); let _ = self.wait_document_ready(Some(DEFAULT_PAGE_LOAD_TIMEOUT)); }, }; // 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 = ServoUrl::parse(¶meters.url) .map(|url| url.into_url()) .map_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: Option<u64>) -> WebDriverResult<WebDriverResponse> { let timeout_channel = match timeout { Some(timeout) => after(Duration::from_millis(timeout)), None => crossbeam_channel::never(), }; 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(timeout_channel) -> _ => 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) = generic_channel::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)?, ))) } /// <https://w3c.github.io/webdriver/#get-window-rect> fn handle_window_rect( &self, verify: VerifyBrowsingContextIsOpen, ) -> WebDriverResult<WebDriverResponse> { let (sender, receiver) = generic_channel::oneshot().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_oneshot_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) = generic_channel::oneshot().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_oneshot_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) = generic_channel::oneshot().unwrap(); self.send_message_to_embedder(WebDriverCommandMsg::MaximizeWebView(webview_id, sender))?; let window_rect = wait_for_oneshot_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) = generic_channel::channel().unwrap(); self.browsing_context_script_command( WebDriverScriptCommand::IsEnabled(element.to_string(), sender), VerifyBrowsingContextIsOpen::No, )?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } 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) = generic_channel::channel().unwrap(); self.browsing_context_script_command( WebDriverScriptCommand::IsSelected(element.to_string(), sender), VerifyBrowsingContextIsOpen::No, )?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } /// <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) = generic_channel::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) = generic_channel::oneshot().unwrap(); self.send_message_to_embedder(WebDriverCommandMsg::GetAllWebViews(sender)) .unwrap(); wait_for_oneshot_response(receiver).unwrap_or_default() } /// <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) = generic_channel::oneshot().unwrap(); let cmd_msg = WebDriverCommandMsg::CloseWebView(webview_id, sender); self.send_message_to_embedder(cmd_msg)?; wait_for_oneshot_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) = generic_channel::oneshot().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)?; // Step 4. Let type hint be the result of getting the property "type" from // parameters. let type_hint = match parameters.type_hint.as_deref() { Some("tab") => NewWindowTypeHint::Tab, Some("window") => NewWindowTypeHint::Window, _ => NewWindowTypeHint::Auto, }; // Step 5. Create a new top-level browsing context by running the window open // steps with URL set to "about:blank", target set to the empty string, and // features set to "noopener" and the user agent configured to create a new // browsing context. This must be done without invoking the focusing steps for the // created browsing context. If type hint has the value "tab", and the // implementation supports multiple browsing context in the same OS window, the // new browsing context should share an OS window with session's current browsing // context. If type hint is "window", and the implementation supports multiple // browsing contexts in separate OS windows, the created browsing context should // be in a new OS window. In all other cases the details of how the browsing // context is presented to the user are implementation defined. self.send_message_to_embedder(WebDriverCommandMsg::NewWindow( type_hint, sender, Some(self.load_status_sender.clone()), ))?; 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) = generic_channel::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. let browsing_context_id = wait_for_ipc_response_flatten(receiver)?; 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) } /// <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) = generic_channel::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()?)?; let browsing_context_id = wait_for_ipc_response_flatten(receiver)?; 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) } /// <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) } /// The boolean in callback result indicates whether implicit_wait can early return /// before timeout with current result. fn implicit_wait<T>( &self, callback: impl Fn() -> Result<(bool, T), (bool, WebDriverError)>, ) -> Result<T, WebDriverError> { let now = Instant::now(); let (implicit_wait, sleep_interval) = { let timeouts = self.session()?.session_timeouts(); ( timeouts .implicit_wait .map_or(Duration::MAX, Duration::from_millis), Duration::from_millis(timeouts.sleep_interval), ) }; loop { match callback() { Ok((can_early_return, value)) => { if can_early_return || now.elapsed() >= implicit_wait { return Ok(value); } }, Err((can_early_return, error)) => { if can_early_return || now.elapsed() >= implicit_wait { return Err(error); } }, } sleep(sleep_interval); } } /// <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()?)?; self.implicit_wait(|| { let (sender, receiver) = generic_channel::channel().unwrap(); let cmd = match parameters.using { LocatorStrategy::CSSSelector => WebDriverScriptCommand::FindElementsCSSSelector( parameters.value.clone(), sender, ), LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => { WebDriverScriptCommand::FindElementsLinkText( parameters.value.clone(), parameters.using == LocatorStrategy::PartialLinkText, sender, ) }, LocatorStrategy::TagName => { WebDriverScriptCommand::FindElementsTagName(parameters.value.clone(), sender) }, LocatorStrategy::XPath => WebDriverScriptCommand::FindElementsXpathSelector( parameters.value.clone(), sender, ), }; self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No) .map_err(|error| (ImplicitWait::Return.into(), error))?; wait_for_ipc_response_flatten(receiver) .map(|value| (!value.is_empty(), value)) .map_err(|error| (ImplicitWait::Return.into(), error)) }) .and_then(|response| { let resp_value: Vec<WebElement> = response.into_iter().map(WebElement).collect(); Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(resp_value)?, ))) }) } /// <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()?)?; self.implicit_wait(|| { let (sender, receiver) = generic_channel::channel().unwrap(); let cmd = match parameters.using { LocatorStrategy::CSSSelector => { WebDriverScriptCommand::FindElementElementsCSSSelector( parameters.value.clone(), element.to_string(), sender, ) }, LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => { WebDriverScriptCommand::FindElementElementsLinkText( parameters.value.clone(), element.to_string(), parameters.using == LocatorStrategy::PartialLinkText, sender, ) }, LocatorStrategy::TagName => WebDriverScriptCommand::FindElementElementsTagName( parameters.value.clone(), element.to_string(), sender, ), LocatorStrategy::XPath => WebDriverScriptCommand::FindElementElementsXPathSelector( parameters.value.clone(), element.to_string(), sender, ), }; self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No) .map_err(|error| (ImplicitWait::Return.into(), error))?; wait_for_ipc_response_flatten(receiver) .map(|value| (!value.is_empty(), value)) .map_err(|error| (ImplicitWait::Return.into(), error)) }) .and_then(|response| { let resp_value: Vec<Value> = response .into_iter() .map(|x| serde_json::to_value(WebElement(x)).unwrap()) .collect(); Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(resp_value)?, ))) }) } /// <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()?)?; self.implicit_wait(|| { let (sender, receiver) = generic_channel::channel().unwrap(); let cmd = match parameters.using { LocatorStrategy::CSSSelector => { WebDriverScriptCommand::FindShadowElementsCSSSelector( parameters.value.clone(), shadow_root.to_string(), sender, ) }, LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => { WebDriverScriptCommand::FindShadowElementsLinkText( parameters.value.clone(), shadow_root.to_string(), parameters.using == LocatorStrategy::PartialLinkText, sender, ) }, LocatorStrategy::TagName => WebDriverScriptCommand::FindShadowElementsTagName( parameters.value.clone(), shadow_root.to_string(), sender, ), LocatorStrategy::XPath => WebDriverScriptCommand::FindShadowElementsXPathSelector( parameters.value.clone(), shadow_root.to_string(), sender, ), }; self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No) .map_err(|error| (ImplicitWait::Return.into(), error))?; wait_for_ipc_response_flatten(receiver) .map(|value| (!value.is_empty(), value)) .map_err(|error| (ImplicitWait::Return.into(), error)) }) .and_then(|response| { let resp_value: Vec<Value> = response .into_iter() .map(|x| serde_json::to_value(WebElement(x)).unwrap()) .collect(); Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(resp_value)?, ))) }) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetElementShadowRoot(element.to_string(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; // Step 5. If shadow root is null, return error with error code no such shadow root. let Some(value) = wait_for_ipc_response_flatten(receiver)? else { return Err(WebDriverError::new(ErrorStatus::NoSuchShadowRoot, "")); }; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(ShadowRoot(value))?, ))) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetElementRect(element.to_string(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; let rect = wait_for_ipc_response_flatten(receiver)?; let response = ElementRectResponse { x: rect.origin.x, y: rect.origin.y, width: rect.size.width, height: rect.size.height, }; Ok(WebDriverResponse::ElementRect(response)) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetElementText(element.to_string(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } ///<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) = generic_channel::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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetComputedRole(element.to_string(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetElementTagName(element.to_string(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetElementAttribute( element.to_string(), name.to_owned(), sender, ); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetElementProperty( element.to_string(), name.to_owned(), sender, ); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(SendableJSValue(wait_for_ipc_response_flatten(receiver)?))?, ))) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetElementCSS(element.to_string(), name.to_owned(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetCookies(sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; let cookies = wait_for_ipc_response_flatten(receiver)?; 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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetCookie(name, sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; let cookies = wait_for_ipc_response_flatten(receiver)?; 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) = generic_channel::channel().unwrap(); // Step 6. cookie expiry time is not an integer type, // or it less than 0 or greater than the maximum safe integer, // return error with error code invalid argument. if let Some(ref expiry) = params.expiry && expiry.0 > MAXIMUM_SAFE_INTEGER { return Err(WebDriverError::new( ErrorStatus::InvalidArgument, "expiry time greater than maximum safe integer", )); } 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 { let datetime = OffsetDateTime::from_unix_timestamp(expiry.0 as i64).map_err(|_| { WebDriverError::new(ErrorStatus::InvalidArgument, "invalid expiry time") })?; 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)?; wait_for_ipc_response_flatten(receiver)?; Ok(WebDriverResponse::Void) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::DeleteCookie(name, sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; wait_for_ipc_response_flatten(receiver)?; Ok(WebDriverResponse::Void) } /// <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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::DeleteCookies(sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; wait_for_ipc_response_flatten(receiver)?; Ok(WebDriverResponse::Void) } /// <https://w3c.github.io/webdriver/#get-timeouts> fn handle_get_timeouts(&mut self) -> WebDriverResult<WebDriverResponse> { let timeouts = self.session()?.session_timeouts(); // FIXME: The specification says that all of these values can be `null`, but the `webdriver` crate // only supports setting `script` as null. When set to null, report these values as being the // default ones for now. // Waiting for version bump together with geckodriver. let timeouts = TimeoutsResponse { script: timeouts.script, page_load: timeouts.page_load.unwrap_or(DEFAULT_PAGE_LOAD_TIMEOUT), implicit: timeouts.implicit_wait.unwrap_or(DEFAULT_IMPLICIT_WAIT), }; Ok(WebDriverResponse::Timeouts(timeouts)) } /// <https://w3c.github.io/webdriver/#set-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 = Some(timeout); } if let Some(timeout) = parameters.implicit { session.session_timeouts_mut().implicit_wait = Some(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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::GetPageSource(sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(wait_for_ipc_response_flatten(receiver)?)?, ))) } /// <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.actions); // 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 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. self.handle_any_user_prompts(self.webview_id()?)?; // 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 = self .session_mut()? .input_cancel_list .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. self.session_mut()?.input_state_table.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!( r#"(async function(__wd_eid) {{ try {{ let result = (async function() {{ {func_body} }})({}); let value = await result; if (window.__wd_eid === __wd_eid) {{ window.webdriverCallback(value); }} }} catch (err) {{ if (window.__wd_eid === __wd_eid) {{ window.webdriverException(err); }} }} }})(window.__wd_eid = (window.__wd_eid || 0) + 1);"#, 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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::ExecuteScriptWithCallback(script, sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; let timeout_duration = self .session()? .session_timeouts() .script .map_or(Duration::MAX, Duration::from_millis); let result = wait_for_script_ipc_response_with_timeout(receiver, timeout_duration)?; self.javascript_evaluation_result_to_webdriver_response(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 (function_body, mut args_string) = self.extract_script_arguments(parameters)?; args_string.push("resolve".to_string()); let joined_args = args_string.join(", "); let script = format!( r#"(async function(__wd_eid) {{ try {{ let result = new Promise(function(resolve, reject) {{ (async function() {{ {function_body} }})({joined_args}) .catch(reject) }}); let value = await result; if (window.__wd_eid === __wd_eid) {{ window.webdriverCallback(value); }} }} catch (err) {{ if (window.__wd_eid === __wd_eid) {{ window.webdriverException(err); }} }} }})(window.__wd_eid = (window.__wd_eid || 0) + 1);"#, ); 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) = generic_channel::channel().unwrap(); self.browsing_context_script_command( WebDriverScriptCommand::ExecuteScriptWithCallback(script, sender), VerifyBrowsingContextIsOpen::No, )?; let timeout_duration = self .session()? .session_timeouts() .script .map_or(Duration::MAX, Duration::from_millis); let result = wait_for_script_ipc_response_with_timeout(receiver, timeout_duration)?; self.javascript_evaluation_result_to_webdriver_response(result) } fn javascript_evaluation_result_to_webdriver_response( &self, result: WebDriverJSResult, ) -> WebDriverResult<WebDriverResponse> { match result { Ok(value) => Ok(WebDriverResponse::Generic(ValueResponse( serde_json::to_value(SendableJSValue(value))?, ))), Err(error) => { let message = format!("{error:?}"); let status = match error { JavaScriptEvaluationError::DocumentNotFound => ErrorStatus::NoSuchWindow, JavaScriptEvaluationError::CompilationFailure => ErrorStatus::JavascriptError, JavaScriptEvaluationError::EvaluationFailure(Some(error_info)) => { return Err(WebDriverError::new_with_data( ErrorStatus::JavascriptError, error_info.message, None, error_info.stack, )); }, JavaScriptEvaluationError::EvaluationFailure(None) => { ErrorStatus::JavascriptError }, JavaScriptEvaluationError::InternalError => ErrorStatus::JavascriptError, JavaScriptEvaluationError::SerializationError(serialization_error) => { match serialization_error { JavaScriptEvaluationResultSerializationError::DetachedShadowRoot => { ErrorStatus::DetachedShadowRoot }, JavaScriptEvaluationResultSerializationError::OtherJavaScriptError => { ErrorStatus::JavascriptError }, JavaScriptEvaluationResultSerializationError::StaleElementReference => { ErrorStatus::StaleElementReference }, JavaScriptEvaluationResultSerializationError::UnknownType => { ErrorStatus::UnsupportedOperation }, } }, JavaScriptEvaluationError::WebViewNotReady => ErrorStatus::NoSuchWindow, }; Err(WebDriverError::new(status, message)) }, } } /// <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) = generic_channel::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_flatten(receiver)? { 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 .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.extract_an_action_sequence(vec![action_sequence]); if let Err(e) = self.dispatch_actions(actions_by_tick, self.browsing_context_id()?) { error!("handle_element_send_keys: dispatch_actions failed: {:?}", e); } }, DispatchStringEvent::Composition(event) => { self.send_blocking_input_event_to_embedder(InputEvent::Ime( ImeEvent::Composition(event), )); }, } } // Step 14. Remove an input source with input state and input id. // It is possible that we only dispatched keydown. // In that case, we cannot remove the id from input state table. // This is a bug in spec: https://github.com/servo/servo/issues/37579#issuecomment-2990762713 if self .session()? .input_cancel_list .iter() .all(|(cancel_item_id, _)| &id != cancel_item_id) { self.session_mut()?.input_state_table.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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::ElementClear(element.to_string(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; wait_for_ipc_response_flatten(receiver)?; Ok(WebDriverResponse::Void) } /// <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) = generic_channel::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_flatten(receiver)? { 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), } } /// <https://w3c.github.io/webdriver/#element-click> /// Step 8 for elements other than <option>, fn perform_element_click(&mut self, element: String) -> WebDriverResult<WebDriverResponse> { // Step 8.1 - 8.4: Create UUID, create input source "pointer". let id = Uuid::new_v4().to_string(); let pointer_ids = self.session()?.pointer_ids(); #[cfg(not(any(target_env = "ohos", target_os = "android")))] let pointer_type = PointerType::Mouse; // There is currently no spec for touchscreen element click support, // which assumes mouse event. // There is an ongoing discussion in W3C: // <https://github.com/w3c/webdriver/issues/1925> #[cfg(any(target_env = "ohos", target_os = "android"))] let pointer_type = PointerType::Touch; self.session_mut()?.input_state_table.insert( id.clone(), InputSourceState::Pointer(PointerInputState::new(pointer_type, pointer_ids, 0.0, 0.0)), ); // 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: ELEMENT_CLICK_BUTTON, ..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: ELEMENT_CLICK_BUTTON, ..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.extract_an_action_sequence(vec![action_sequence]); if let Err(e) = self.dispatch_actions(actions_by_tick, self.browsing_context_id()?) { 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.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, }); if rect.as_ref().is_some_and(Rect::is_empty) { return Err(WebDriverError::new( ErrorStatus::UnknownError, "The requested `rect` has zero width and/or height", )); } let webview_id = self.webview_id()?; let (sender, receiver) = crossbeam_channel::unbounded(); self.send_message_to_embedder(WebDriverCommandMsg::TakeScreenshot( webview_id, rect, sender, ))?; let result = match receiver.recv_timeout(SCREENSHOT_TIMEOUT) { Ok(result) => Ok(result), Err(RecvTimeoutError::Timeout) => Err(WebDriverError::new( ErrorStatus::Timeout, "Timed out waiting to take screenshot. Test likely didn't finish.", )), Err(RecvTimeoutError::Disconnected) => Err(WebDriverError::new( ErrorStatus::UnknownError, "Could not take screenshot because channel disconnected.", )), }?; let image = result.map_err(|error| { WebDriverError::new( ErrorStatus::UnknownError, format!("Failed to take screenshot: {error:?}"), ) })?; let mut png_data = Cursor::new(Vec::new()); DynamicImage::ImageRgba8(image) .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) = generic_channel::channel().unwrap(); let cmd = WebDriverScriptCommand::ScrollAndGetBoundingClientRect(element.to_string(), sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::Yes)?; let rect = wait_for_ipc_response_flatten(receiver)?; // 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)?, ))) } /// <https://html.spec.whatwg.org/multipage/#set-rph-registration-mode> fn handle_custom_handlers_set_mode( &self, parameters: &CustomHandlersSetModeParameters, ) -> WebDriverResult<WebDriverResponse> { // Step 2. Let mode be the result of getting a property named "mode" from parameters. // Step 3. If mode is not "autoAccept", "autoReject", or "none", return a WebDriver error with WebDriver error code invalid argument. let mode = match parameters.mode.as_str() { "autoAccept" => CustomHandlersAutomationMode::AutoAccept, "autoReject" => CustomHandlersAutomationMode::AutoReject, "none" => CustomHandlersAutomationMode::None, _ => { return Err(WebDriverError::new( ErrorStatus::InvalidArgument, "invalid argument", )); }, }; // Step 4. Let document be the current browsing context's active document. // Step 5. Set document's registerProtocolHandler() automation mode to mode. self.top_level_script_command( WebDriverScriptCommand::SetProtocolHandlerAutomationMode(mode), VerifyBrowsingContextIsOpen::Yes, )?; // Step 6. Return success with data null. Ok(WebDriverResponse::Void) } 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() { (self.default_preferences.clone(), BTreeMap::new()) } else { // If we only want to reset some of the preferences. let mut new_preferences = prefs::get().clone(); for key in parameters.prefs.iter() { new_preferences.set_value(key, self.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 handle_shutdown(&self) -> WebDriverResult<WebDriverResponse> { self.send_message_to_embedder(WebDriverCommandMsg::Shutdown)?; Ok(WebDriverResponse::Void) } fn handle_reset_all_cookies(&self) -> WebDriverResult<WebDriverResponse> { let (sender, receiver) = unbounded(); self.send_message_to_embedder(WebDriverCommandMsg::ResetAllCookies(sender))?; if receiver.recv().is_err() { log::warn!("Communication failure while clearing cookies; status unknown"); } Ok(WebDriverResponse::Void) } fn verify_top_level_browsing_context_is_open( &self, webview_id: WebViewId, ) -> Result<(), WebDriverError> { let (sender, receiver) = generic_channel::oneshot().unwrap(); self.send_message_to_embedder(WebDriverCommandMsg::IsWebViewOpen(webview_id, sender))?; if wait_for_oneshot_response(receiver)? { Ok(()) } else { Err(WebDriverError::new(ErrorStatus::NoSuchWindow, "")) } } fn verify_browsing_context_is_open( &self, browsing_context_id: BrowsingContextId, ) -> Result<(), WebDriverError> { let (sender, receiver) = generic_channel::oneshot().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 wait_until_browsing_context_is_open( &self, browsing_context_id: BrowsingContextId, ) -> WebDriverResult<()> { // We cannot use provided timeout from configuration, as `fn wait_until_browsing_context_is_open` is not a standard step in spec. // Some tests use `page_load = 0` deliberately, which would always fail with the function. const OPEN_BROWSING_CONTEXT_TIMEOUT: Duration = Duration::from_millis(DEFAULT_PAGE_LOAD_TIMEOUT); let now = Instant::now(); let timeouts = self.session()?.session_timeouts(); let sleep_interval = Duration::from_millis(timeouts.sleep_interval); while now.elapsed() < OPEN_BROWSING_CONTEXT_TIMEOUT { if self .verify_browsing_context_is_open(browsing_context_id) .is_ok() { return Ok(()); } sleep(sleep_interval); } Err(WebDriverError::new( ErrorStatus::Timeout, format!( "Timed out waiting for the top-level browsing context {browsing_context_id} to be ready" ), )) } 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/delete a new session, check status, or shutdown Servo, // we need to ensure that a session has previously been created. match msg.command { WebDriverCommand::NewSession(_) | WebDriverCommand::Status | WebDriverCommand::DeleteSession | WebDriverCommand::Extension(ServoExtensionCommand::Shutdown) | WebDriverCommand::Extension(ServoExtensionCommand::ResetAllCookies) => {}, _ => { 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(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), ServoExtensionCommand::CustomHandlersSetMode(ref x) => { self.handle_custom_handlers_set_mode(x) }, ServoExtensionCommand::Shutdown => self.handle_shutdown(), ServoExtensionCommand::ResetAllCookies => self.handle_reset_all_cookies(), }, _ => Err(WebDriverError::new( ErrorStatus::UnsupportedOperation, format!("Command not implemented: {:?}", msg.command), )), } } fn teardown_session(&mut self, _session: SessionTeardownKind) { self.session = None; } } fn wait_for_oneshot_response<T>( receiver: generic_channel::GenericOneshotReceiver<T>, ) -> Result<T, WebDriverError> where T: for<'de> Deserialize<'de> + Serialize, { receiver .recv() .map_err(|_| WebDriverError::new(ErrorStatus::NoSuchWindow, "")) } fn wait_for_ipc_response<T>(receiver: GenericReceiver<T>) -> Result<T, WebDriverError> where T: for<'de> Deserialize<'de> + Serialize, { receiver .recv() .map_err(|_| WebDriverError::new(ErrorStatus::NoSuchWindow, "")) } /// This function is like `wait_for_ipc_response`, but works on a channel that /// returns a `Result<T, ErrorStatus>`, mapping all errors into `WebDriverError`. fn wait_for_ipc_response_flatten<T>( receiver: GenericReceiver<Result<T, ErrorStatus>>, ) -> Result<T, WebDriverError> where T: for<'de> Deserialize<'de> + Serialize, { match receiver.recv() { Ok(Ok(value)) => Ok(value), Ok(Err(error_status)) => Err(WebDriverError::new(error_status, "")), Err(_) => Err(WebDriverError::new(ErrorStatus::NoSuchWindow, "")), } } fn wait_for_script_ipc_response_with_timeout<T>( receiver: GenericReceiver<T>, timeout: Duration, ) -> Result<T, WebDriverError> where T: for<'de> Deserialize<'de> + Serialize, { receiver .try_recv_timeout(timeout) .map_err(|error| match error { generic_channel::TryReceiveError::ReceiveError(_) => { WebDriverError::new(ErrorStatus::NoSuchWindow, "") }, generic_channel::TryReceiveError::Empty => { WebDriverError::new(ErrorStatus::ScriptTimeout, "") }, }) } 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() }, }, } }