diff --git a/components/webdriver_server/capabilities.rs b/components/webdriver_server/capabilities.rs index 32596f5275a..f1cc151c81c 100644 --- a/components/webdriver_server/capabilities.rs +++ b/components/webdriver_server/capabilities.rs @@ -27,7 +27,7 @@ impl ServoCapabilities { set_window_rect: true, strict_file_interactability: false, accept_proxy: false, - accept_custom: false, + accept_custom: true, } } } @@ -59,7 +59,13 @@ impl BrowserCapabilities for ServoCapabilities { Ok(self.set_window_rect) } - fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult { + fn strict_file_interactability(&mut self, value: &Capabilities) -> WebDriverResult { + if let Some(Value::Bool(strict_file_interactability)) = + value.get("strictFileInteractability") + { + self.strict_file_interactability = *strict_file_interactability; + } + Ok(self.strict_file_interactability) } diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs index 830b1f59d86..30f3cdb55b0 100644 --- a/components/webdriver_server/lib.rs +++ b/components/webdriver_server/lib.rs @@ -8,6 +8,8 @@ mod actions; mod capabilities; +mod session; +mod timeout; mod user_prompt; use std::borrow::ToOwned; @@ -67,9 +69,9 @@ use webdriver::response::{ use webdriver::server::{self, Session, SessionTeardownKind, WebDriverHandler}; use crate::actions::{ActionItem, InputSourceState, PointerInputState}; -use crate::user_prompt::{ - UserPromptHandler, default_unhandled_prompt_behavior, deserialize_unhandled_prompt_behaviour, -}; +use crate::session::PageLoadStrategy; +use crate::timeout::TimeoutsConfiguration; +use crate::user_prompt::UserPromptHandler; #[derive(Default)] pub struct WebDriverMessageIdGenerator { @@ -173,18 +175,9 @@ pub struct WebDriverSession { /// window_handles: HashMap, - /// Time to wait for injected scripts to run before interrupting them. A [`None`] value - /// specifies that the script should run indefinitely. - script_timeout: Option, + timeouts: TimeoutsConfiguration, - /// Time to wait for a page to finish loading upon navigation. - load_timeout: u64, - - /// Time to wait for the element location strategy when retrieving elements, and when - /// waiting for an element to become interactable. - implicit_wait_timeout: u64, - - page_loading_strategy: String, + page_loading_strategy: PageLoadStrategy, strict_file_interactability: bool, @@ -207,17 +200,11 @@ impl WebDriverSession { id: Uuid::new_v4(), webview_id, browsing_context_id, - window_handles, - - script_timeout: Some(30_000), - load_timeout: 300_000, - implicit_wait_timeout: 0, - - page_loading_strategy: "normal".to_string(), + timeouts: TimeoutsConfiguration::default(), + page_loading_strategy: PageLoadStrategy::Normal, strict_file_interactability: false, user_prompt_handler: UserPromptHandler::new(), - input_state_table: RefCell::new(HashMap::new()), input_cancel_list: RefCell::new(Vec::new()), } @@ -562,143 +549,53 @@ impl Handler { thread::sleep(Duration::from_secs(seconds)); } - let mut servo_capabilities = ServoCapabilities::new(); - let processed_capabilities = parameters.match_browser(&mut servo_capabilities)?; - // 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() { - Err(WebDriverError::new( + return Err(WebDriverError::new( ErrorStatus::SessionNotCreated, "Session already created", - )) - } else { - match processed_capabilities { - Some(mut processed) => { - let webview_id = self.focus_webview_id()?; - let browsing_context_id = BrowsingContextId::from(webview_id); - let mut session = WebDriverSession::new(browsing_context_id, webview_id); + )); + } - match processed.get("pageLoadStrategy") { - Some(strategy) => session.page_loading_strategy = strategy.to_string(), - None => { - processed.insert( - "pageLoadStrategy".to_string(), - json!(session.page_loading_strategy), - ); - }, - } + // Step 2. Skip because the step is only applied to an intermediary node. + // Step 3. Skip since all sessions are http for now. - match processed.get("strictFileInteractability") { - Some(strict_file_interactability) => { - session.strict_file_interactability = - strict_file_interactability.as_bool().unwrap() - }, - None => { - processed.insert( - "strictFileInteractability".to_string(), - json!(session.strict_file_interactability), - ); - }, - } + // 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)?; - match processed.get("proxy") { - Some(_) => (), - None => { - processed.insert("proxy".to_string(), json!({})); - }, - } - - if let Some(timeouts) = processed.get("timeouts") { - if let Some(script_timeout_value) = timeouts.get("script") { - session.script_timeout = script_timeout_value.as_u64(); - } - if let Some(load_timeout_value) = timeouts.get("pageLoad") { - if let Some(load_timeout) = load_timeout_value.as_u64() { - session.load_timeout = load_timeout; - } - } - if let Some(implicit_wait_timeout_value) = timeouts.get("implicit") { - if let Some(implicit_wait_timeout) = - implicit_wait_timeout_value.as_u64() - { - session.implicit_wait_timeout = implicit_wait_timeout; - } - } - } - processed.insert( - "timeouts".to_string(), - json!({ - "script": session.script_timeout, - "pageLoad": session.load_timeout, - "implicit": session.implicit_wait_timeout, - }), - ); - - match processed.get("acceptInsecureCerts") { - Some(_accept_insecure_certs) => { - // FIXME do something here? - }, - None => { - processed.insert( - "acceptInsecureCerts".to_string(), - json!(servo_capabilities.accept_insecure_certs), - ); - }, - } - - match processed.get("unhandledPromptBehavior") { - Some(unhandled_prompt_behavior) => { - session.user_prompt_handler = deserialize_unhandled_prompt_behaviour( - unhandled_prompt_behavior.clone(), - )?; - }, - None => { - processed.insert( - "unhandledPromptBehavior".to_string(), - json!(default_unhandled_prompt_behavior()), - ); - }, - } - - processed.insert( - "browserName".to_string(), - json!(servo_capabilities.browser_name), - ); - processed.insert( - "browserVersion".to_string(), - json!(servo_capabilities.browser_version), - ); - processed.insert( - "platformName".to_string(), - json!( - servo_capabilities - .platform_name - .unwrap_or("unknown".to_string()) - ), - ); - processed.insert( - "setWindowRect".to_string(), - json!(servo_capabilities.set_window_rect), - ); - processed.insert( - "userAgent".to_string(), - servo_config::pref!(user_agent).into(), - ); - - let response = - NewSessionResponse::new(session.id.to_string(), Value::Object(processed)); - self.session = Some(session); - Ok(WebDriverResponse::NewSession(response)) - }, - // Step 5. If capabilities's is null, - // return error with error code session not created. - None => Err(WebDriverError::new( + // 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 + // Step 8. Set session' current top-level browsing context + let webview_id = self.focus_webview_id()?; + let browsing_context_id = BrowsingContextId::from(webview_id); + // Create and append session to the handler + let session_id = self.create_session( + &mut capabilities, + &servo_capabilities, + webview_id, + browsing_context_id, + )?; + + // 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 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)) } fn handle_delete_session(&mut self) -> WebDriverResult { @@ -798,7 +695,7 @@ impl Handler { // Step 1. If session's page loading strategy is "none", // return success with data null. - if session.page_loading_strategy == "none" { + if session.page_loading_strategy == PageLoadStrategy::None { return Ok(WebDriverResponse::Void); } @@ -812,7 +709,7 @@ impl Handler { } // Step 3. let timeout be the session's page load timeout. - let timeout = self.session()?.load_timeout; + let timeout = self.session()?.timeouts.page_load; // TODO: Step 4. Implement timer parameter @@ -1886,9 +1783,9 @@ impl Handler { .ok_or(WebDriverError::new(ErrorStatus::SessionNotCreated, ""))?; let timeouts = TimeoutsResponse { - script: session.script_timeout, - page_load: session.load_timeout, - implicit: session.implicit_wait_timeout, + script: session.timeouts.script, + page_load: session.timeouts.page_load, + implicit: session.timeouts.implicit_wait, }; Ok(WebDriverResponse::Timeouts(timeouts)) @@ -1904,13 +1801,13 @@ impl Handler { .ok_or(WebDriverError::new(ErrorStatus::SessionNotCreated, ""))?; if let Some(timeout) = parameters.script { - session.script_timeout = timeout; + session.timeouts.script = timeout; } if let Some(timeout) = parameters.page_load { - session.load_timeout = timeout + session.timeouts.page_load = timeout } if let Some(timeout) = parameters.implicit { - session.implicit_wait_timeout = timeout + session.timeouts.implicit_wait = timeout } Ok(WebDriverResponse::Void) @@ -2054,7 +1951,7 @@ impl Handler { .collect(); args_string.push("resolve".to_string()); - let timeout_script = if let Some(script_timeout) = self.session()?.script_timeout { + let timeout_script = if let Some(script_timeout) = self.session()?.timeouts.script { format!("setTimeout(webdriverTimeout, {});", script_timeout) } else { "".into() diff --git a/components/webdriver_server/session.rs b/components/webdriver_server/session.rs new file mode 100644 index 00000000000..0949c7f5657 --- /dev/null +++ b/components/webdriver_server/session.rs @@ -0,0 +1,173 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use base::id::{BrowsingContextId, WebViewId}; +use serde_json::{Map, Value, json}; +use uuid::Uuid; +use webdriver::error::WebDriverResult; + +use crate::capabilities::ServoCapabilities; +use crate::timeout::{deserialize_as_timeouts_configuration, serialize_timeouts_configuration}; +use crate::user_prompt::{ + default_unhandled_prompt_behavior, deserialize_unhandled_prompt_behaviour, +}; +use crate::{Handler, WebDriverSession}; + +#[derive(Debug, PartialEq, serde::Serialize)] +pub enum PageLoadStrategy { + None, + Eager, + Normal, +} + +// Need a different implementation for ToString than Display +#[allow(clippy::to_string_trait_impl)] +impl ToString for PageLoadStrategy { + fn to_string(&self) -> String { + match self { + PageLoadStrategy::None => String::from("none"), + PageLoadStrategy::Eager => String::from("eager"), + PageLoadStrategy::Normal => String::from("normal"), + } + } +} + +impl Handler { + /// + pub(crate) fn create_session( + &mut self, + capabilities: &mut Map, + servo_capabilities: &ServoCapabilities, + webview_id: WebViewId, + browsing_context_id: BrowsingContextId, + ) -> WebDriverResult { + // Step 2. Let session be a new session + let mut session = WebDriverSession::new(browsing_context_id, webview_id); + + // Step 3. Let proxy be the result of getting property "proxy" from capabilities + match capabilities.get("proxy") { + // Proxy is a proxy configuration object + Some(_) => { + // TODO: + // Take implementation-defined steps to set the user agent proxy + // using the extracted proxy configuration. + // If the defined proxy cannot be configured return error with error code + // session not created. Otherwise set the has proxy configuration flag to true. + }, + // Otherwise, set a property of capabilities with name "proxy" + // and a value that is a new JSON Object. + None => { + capabilities.insert(String::from("proxy"), json!({})); + }, + } + + // Step 4. If capabilites has a property named "acceptInsecureCerts" + match capabilities.get("acceptInsecureCerts") { + Some(_accept_insecure_certs) => { + // TODO: Set the endpoint node's accept insecure TLS flag + }, + None => { + capabilities.insert(String::from("acceptInsecureCerts"), json!(false)); + }, + } + + // Step 5. Let user prompt handler capability be the result of + // getting property "unhandledPromptBehavior" from capabilities + match capabilities.get("unhandledPromptBehavior") { + // Step 6. If user prompt handler capability is not undefined + Some(unhandled_prompt_behavior) => { + session.user_prompt_handler = + deserialize_unhandled_prompt_behaviour(unhandled_prompt_behavior.clone())?; + }, + // Step 7. Let serialized user prompt handler be serialize the user prompt handler. + // Step 8. Set a property on capabilities with the name "unhandledPromptBehavior", + // and the value serialized user prompt handler. + // Ignore because the user prompt handler is already in the capabilities object + None => { + capabilities.insert( + String::from("unhandledPromptBehavior"), + json!(default_unhandled_prompt_behavior()), + ); + }, + } + + // TODO: flag is http by default for now + // Step 9. If flags contains "http" + // Step 9.1. Let strategy be the result of getting property "pageLoadStrategy" from capabilities. + match capabilities.get("pageLoadStrategy") { + // If strategy is a string, set the session's page loading strategy to strategy. + Some(strategy) => match strategy.to_string().as_str() { + "none" => session.page_loading_strategy = PageLoadStrategy::None, + "eager" => session.page_loading_strategy = PageLoadStrategy::Eager, + _ => session.page_loading_strategy = PageLoadStrategy::Normal, + }, + // Otherwise, set the page loading strategy to normal and set a property of capabilities + // with name "pageLoadStrategy" and value "normal". + None => { + capabilities.insert( + String::from("pageLoadStrategy"), + json!(session.page_loading_strategy.to_string()), + ); + session.page_loading_strategy = PageLoadStrategy::Normal; + }, + } + + // Step 9.2. Let strictFileInteractability be the result of getting property + // "strictFileInteractability" from capabilities + if let Some(Value::Bool(strict_file_interactability)) = + capabilities.get("strictFileInteractability") + { + session.strict_file_interactability = *strict_file_interactability; + } + + // Step 9.3. Let timeouts be the result of getting a property "timeouts" from capabilities. + // If timeouts is not undefined, set session's session timeouts to timeouts. + if let Some(timeouts) = capabilities.get("timeouts") { + session.timeouts = deserialize_as_timeouts_configuration(timeouts)?; + } + + // Step 9.4 Set a property on capabilities with name "timeouts" + // and value serialize the timeouts configuration with session's session timeouts. + capabilities.insert( + "timeouts".to_string(), + json!(serialize_timeouts_configuration(&session.timeouts)), + ); + + // Step 10. Process any extension capabilities in capabilities in an implementation-defined manner + // Nothing to processed + + // Step 11. Run any WebDriver new session algorithm defined in external specifications + capabilities.insert( + "browserName".to_string(), + json!(servo_capabilities.browser_name), + ); + capabilities.insert( + "browserVersion".to_string(), + json!(servo_capabilities.browser_version), + ); + capabilities.insert( + "platformName".to_string(), + json!( + servo_capabilities + .platform_name + .clone() + .unwrap_or("unknown".to_string()) + ), + ); + capabilities.insert( + "setWindowRect".to_string(), + json!(servo_capabilities.set_window_rect), + ); + capabilities.insert( + "userAgent".to_string(), + servo_config::pref!(user_agent).into(), + ); + + // Step 12. Append session to active sessions + let id = session.id; + self.session = Some(session); + + Ok(id) + } +} diff --git a/components/webdriver_server/timeout.rs b/components/webdriver_server/timeout.rs new file mode 100644 index 00000000000..633ee18f7f8 --- /dev/null +++ b/components/webdriver_server/timeout.rs @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use serde_json::Value; +use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; + +pub(crate) struct TimeoutsConfiguration { + pub script: Option, + pub page_load: u64, + pub implicit_wait: u64, +} + +impl Default for TimeoutsConfiguration { + fn default() -> Self { + TimeoutsConfiguration { + script: Some(30_000), + page_load: 300_000, + implicit_wait: 0, + } + } +} + +/// +pub(crate) fn deserialize_as_timeouts_configuration( + timeouts: &Value, +) -> WebDriverResult { + if let Value::Object(map) = timeouts { + let mut config = TimeoutsConfiguration::default(); + for (key, value) in map { + match key.as_str() { + "implicit" => { + config.implicit_wait = value.as_f64().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Invalid implicit timeout", + ) + })? as u64; + }, + "pageLoad" => { + config.page_load = value.as_f64().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Invalid page load timeout", + ) + })? as u64; + }, + "script" => { + config.script = Some(value.as_f64().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Invalid script timeout") + })? as u64); + }, + _ => { + return Err(WebDriverError::new( + ErrorStatus::UnknownCommand, + "Unknown timeout key", + )); + }, + } + } + Ok(config) + } else { + Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Expected an object for timeouts", + )) + } +} + +pub(crate) fn serialize_timeouts_configuration(timeouts: &TimeoutsConfiguration) -> Value { + let mut map = serde_json::Map::new(); + if let Some(script_timeout) = timeouts.script { + map.insert("script".to_string(), Value::from(script_timeout)); + } + map.insert("pageLoad".to_string(), Value::from(timeouts.page_load)); + map.insert("implicit".to_string(), Value::from(timeouts.implicit_wait)); + Value::Object(map) +} diff --git a/tests/wpt/meta/webdriver/tests/classic/new_session/create_alwaysMatch.py.ini b/tests/wpt/meta/webdriver/tests/classic/new_session/create_alwaysMatch.py.ini deleted file mode 100644 index c837e638bb0..00000000000 --- a/tests/wpt/meta/webdriver/tests/classic/new_session/create_alwaysMatch.py.ini +++ /dev/null @@ -1,21 +0,0 @@ -[create_alwaysMatch.py] - [test_valid[timeouts-value12\]] - expected: FAIL - - [test_valid[strictFileInteractability-True\]] - expected: FAIL - - [test_valid[test:extension-False\]] - expected: FAIL - - [test_valid[test:extension-abc\]] - expected: FAIL - - [test_valid[test:extension-123\]] - expected: FAIL - - [test_valid[test:extension-value22\]] - expected: FAIL - - [test_valid[test:extension-value23\]] - expected: FAIL diff --git a/tests/wpt/meta/webdriver/tests/classic/new_session/create_firstMatch.py.ini b/tests/wpt/meta/webdriver/tests/classic/new_session/create_firstMatch.py.ini deleted file mode 100644 index 3752bebca7b..00000000000 --- a/tests/wpt/meta/webdriver/tests/classic/new_session/create_firstMatch.py.ini +++ /dev/null @@ -1,21 +0,0 @@ -[create_firstMatch.py] - [test_valid[timeouts-value12\]] - expected: FAIL - - [test_valid[strictFileInteractability-True\]] - expected: FAIL - - [test_valid[test:extension-False\]] - expected: FAIL - - [test_valid[test:extension-abc\]] - expected: FAIL - - [test_valid[test:extension-123\]] - expected: FAIL - - [test_valid[test:extension-value22\]] - expected: FAIL - - [test_valid[test:extension-value23\]] - expected: FAIL