From 9effdce5a17a81fe814348bd7a780d2e42d02ef0 Mon Sep 17 00:00:00 2001 From: batu_hoang <55729155+longvatrong111@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:57:19 +0800 Subject: [PATCH] Implement webdriver extract script arguments (#38357) Fix script parsing step. Implement webdriver `extract script arguments`. Implement `deserialize_web_element` and `deserialize_shadow_root` from script command argument. Testing: `/tests/wpt/tests/webdriver/tests/classic/execute_script/` `/tests/wpt/tests/webdriver/tests/classic/execute_async_script/` cc: @xiaochengh --------- Signed-off-by: batu_hoang --- components/script/dom/window.rs | 19 ++- components/script/script_thread.rs | 16 ++ components/script/webdriver_handlers.rs | 24 +++ .../script_bindings/webidls/Window.webidl | 2 +- components/shared/embedder/webdriver.rs | 2 + components/webdriver_server/elements.rs | 144 ++++++++++++++++++ components/webdriver_server/lib.rs | 106 +++---------- .../execute_async_script/arguments.py.ini | 39 ----- .../classic/execute_script/arguments.py.ini | 45 ------ 9 files changed, 228 insertions(+), 169 deletions(-) create mode 100644 components/webdriver_server/elements.rs diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index e78ded5ba5f..88e35eecdff 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -1524,9 +1524,22 @@ impl WindowMethods for Window { .map(Root::upcast::) } - fn WebdriverWindow(&self, _id: DOMString) -> Option> { - warn!("Window references are not supported in webdriver yet"); - None + fn WebdriverWindow(&self, id: DOMString) -> Option> { + let window_proxy = self.window_proxy.get()?; + + // Window must be top level browsing context. + if window_proxy.browsing_context_id() != window_proxy.webview_id() { + return None; + } + + let pipeline_id = window_proxy.currently_active()?; + let document = ScriptThread::find_document(pipeline_id)?; + + if document.upcast::().unique_id(pipeline_id) == id.str() { + Some(DomRoot::from_ref(&window_proxy)) + } else { + None + } } fn WebdriverShadowRoot(&self, id: DOMString) -> Option> { diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index ee57b0f2307..07c0174e1f7 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -2396,6 +2396,22 @@ impl ScriptThread { can_gc, ) }, + WebDriverScriptCommand::GetKnownElement(element_id, reply) => { + webdriver_handlers::handle_get_known_element( + &documents, + pipeline_id, + element_id, + reply, + ) + }, + WebDriverScriptCommand::GetKnownShadowRoot(element_id, reply) => { + webdriver_handlers::handle_get_known_shadow_root( + &documents, + pipeline_id, + element_id, + reply, + ) + }, WebDriverScriptCommand::GetActiveElement(reply) => { webdriver_handlers::handle_get_active_element(&documents, pipeline_id, reply) }, diff --git a/components/script/webdriver_handlers.rs b/components/script/webdriver_handlers.rs index 8d54331be8c..737fc784a51 100644 --- a/components/script/webdriver_handlers.rs +++ b/components/script/webdriver_handlers.rs @@ -122,6 +122,18 @@ fn is_disabled(element: &Element) -> bool { element.is_actually_disabled() } +pub(crate) fn handle_get_known_shadow_root( + documents: &DocumentCollection, + pipeline: PipelineId, + shadow_root_id: String, + reply: IpcSender>, +) { + let result = get_known_shadow_root(documents, pipeline, shadow_root_id).map(|_| ()); + if reply.send(result).is_err() { + error!("Webdriver get known shadow root reply failed"); + } +} + /// fn get_known_shadow_root( documents: &DocumentCollection, @@ -170,6 +182,18 @@ fn get_known_shadow_root( Ok(shadow_root) } +pub(crate) fn handle_get_known_element( + documents: &DocumentCollection, + pipeline: PipelineId, + element_id: String, + reply: IpcSender>, +) { + let result = get_known_element(documents, pipeline, element_id).map(|_| ()); + if reply.send(result).is_err() { + error!("Webdriver get known element reply failed"); + } +} + /// fn get_known_element( documents: &DocumentCollection, diff --git a/components/script_bindings/webidls/Window.webidl b/components/script_bindings/webidls/Window.webidl index 929279f7951..ec95eadfab5 100644 --- a/components/script_bindings/webidls/Window.webidl +++ b/components/script_bindings/webidls/Window.webidl @@ -152,7 +152,7 @@ partial interface Window { undefined webdriverTimeout(); Element? webdriverElement(DOMString id); Element? webdriverFrame(DOMString id); - Window? webdriverWindow(DOMString id); + WindowProxy? webdriverWindow(DOMString id); ShadowRoot? webdriverShadowRoot(DOMString id); }; diff --git a/components/shared/embedder/webdriver.rs b/components/shared/embedder/webdriver.rs index 3b1afb3c0e4..7f9233ea664 100644 --- a/components/shared/embedder/webdriver.rs +++ b/components/shared/embedder/webdriver.rs @@ -210,6 +210,8 @@ pub enum WebDriverScriptCommand { FindShadowElementsXPathSelector(String, String, IpcSender, ErrorStatus>>), GetElementShadowRoot(String, IpcSender, ErrorStatus>>), ElementClick(String, IpcSender, ErrorStatus>>), + GetKnownElement(String, IpcSender>), + GetKnownShadowRoot(String, IpcSender>), GetActiveElement(IpcSender>), GetComputedRole(String, IpcSender, ErrorStatus>>), GetCookie( diff --git a/components/webdriver_server/elements.rs b/components/webdriver_server/elements.rs new file mode 100644 index 00000000000..61c716192ae --- /dev/null +++ b/components/webdriver_server/elements.rs @@ -0,0 +1,144 @@ +/* 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 embedder_traits::WebDriverScriptCommand; +use ipc_channel::ipc; +use serde_json::Value; +use webdriver::command::JavascriptCommandParameters; +use webdriver::error::{WebDriverError, WebDriverResult}; + +use crate::{Handler, VerifyBrowsingContextIsOpen, wait_for_ipc_response}; + +/// +const ELEMENT_IDENTIFIER: &str = "element-6066-11e4-a52e-4f735466cecf"; +/// +const FRAME_IDENTIFIER: &str = "frame-075b-4da1-b6ba-e579c2d3230a"; +/// +const WINDOW_IDENTIFIER: &str = "window-fcc6-11e5-b4f8-330a88ab9d7f"; +/// +const SHADOW_ROOT_IDENTIFIER: &str = "shadow-6066-11e4-a52e-4f735466cecf"; + +impl Handler { + /// + pub(crate) fn extract_script_arguments( + &self, + parameters: JavascriptCommandParameters, + ) -> WebDriverResult<(String, Vec)> { + // Step 1. Let script be the result of getting a property named "script" from parameters + // Step 2. (Skip) If script is not a String, return error with error code invalid argument. + let script = parameters.script; + + // Step 3. Let args be the result of getting a property named "args" from parameters. + // Step 4. (Skip) If args is not an Array return error with error code invalid argument. + let args: Vec = parameters + .args + .as_deref() + .unwrap_or(&[]) + .iter() + .map(|value| self.json_deserialize(value)) + .collect::>>()?; + + Ok((script, args)) + } + + /// + pub(crate) fn deserialize_web_element(&self, element: &Value) -> WebDriverResult { + // Step 2. Let reference be the result of getting the web element identifier property from object. + let element_ref: String = match element { + Value::String(s) => s.clone(), + _ => element.to_string(), + }; + + // Step 3. Let element be the result of trying to get a known element with session and reference. + let (sender, receiver) = ipc::channel().unwrap(); + self.browsing_context_script_command( + WebDriverScriptCommand::GetKnownElement(element_ref.clone(), sender), + VerifyBrowsingContextIsOpen::No, + )?; + + match wait_for_ipc_response(receiver)? { + // Step 4. Return success with data element. + Ok(_) => Ok(format!("window.webdriverElement(\"{}\")", element_ref)), + Err(err) => Err(WebDriverError::new(err, "No such element")), + } + } + + /// + pub(crate) fn deserialize_shadow_root(&self, shadow_root: &Value) -> WebDriverResult { + // Step 2. Let reference be the result of getting the shadow root identifier property from object. + let shadow_root_ref = match shadow_root { + Value::String(s) => s.clone(), + _ => shadow_root.to_string(), + }; + + // Step 3. Let element be the result of trying to get a known element with session and reference. + let (sender, receiver) = ipc::channel().unwrap(); + self.browsing_context_script_command( + WebDriverScriptCommand::GetKnownShadowRoot(shadow_root_ref.clone(), sender), + VerifyBrowsingContextIsOpen::No, + )?; + + match wait_for_ipc_response(receiver)? { + // Step 4. Return success with data element. + Ok(_) => Ok(format!( + "window.webdriverShadowRoot(\"{}\")", + shadow_root_ref + )), + Err(err) => Err(WebDriverError::new(err, "No such shadowroot")), + } + } + + /// + fn json_deserialize(&self, v: &Value) -> WebDriverResult { + let res = match v { + Value::Null => "null".to_string(), + Value::String(s) => format!("\"{}\"", s), + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + Value::Array(list) => { + let elems = list + .iter() + .map(|v| self.json_deserialize(v)) + .collect::>>()?; + format!("[{}]", elems.join(", ")) + }, + Value::Object(map) => { + let key = map.keys().next().map(String::as_str); + match (key, map.values().next()) { + (Some(ELEMENT_IDENTIFIER), Some(id)) => { + return self.deserialize_web_element(id); + }, + (Some(FRAME_IDENTIFIER), Some(id)) => { + let frame_ref = match id { + Value::String(s) => s.clone(), + _ => id.to_string(), + }; + return Ok(format!("window.webdriverFrame(\"{}\")", frame_ref)); + }, + (Some(WINDOW_IDENTIFIER), Some(id)) => { + let window_ref = match id { + Value::String(s) => s.clone(), + _ => id.to_string(), + }; + return Ok(format!("window.webdriverWindow(\"{}\")", window_ref)); + }, + (Some(SHADOW_ROOT_IDENTIFIER), Some(id)) => { + return self.deserialize_shadow_root(id); + }, + _ => {}, + } + let elems = map + .iter() + .map(|(k, v)| { + let arg = self.json_deserialize(v)?; + Ok(format!("{}: {}", k, arg)) + }) + .collect::>>()?; + format!("{{{}}}", elems.join(", ")) + }, + }; + + Ok(res) + } +} diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs index 8e6b631590a..8a25ba1827f 100644 --- a/components/webdriver_server/lib.rs +++ b/components/webdriver_server/lib.rs @@ -8,6 +8,7 @@ mod actions; mod capabilities; +mod elements; mod session; mod timeout; mod user_prompt; @@ -1955,23 +1956,11 @@ impl Handler { /// fn handle_execute_script( &self, - parameters: &JavascriptCommandParameters, + parameters: JavascriptCommandParameters, ) -> WebDriverResult { - // 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.session()?.browsing_context_id)?; - // Step 3. Handle any user prompt. - self.handle_any_user_prompts(self.session()?.webview_id)?; - - let func_body = ¶meters.script; - let args_string: Vec<_> = parameters - .args - .as_deref() - .unwrap_or(&[]) - .iter() - .map(webdriver_value_to_js_argument) - .collect(); - + // 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. @@ -1982,6 +1971,13 @@ impl Handler { ); 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.session()?.browsing_context_id)?; + + // Step 3. Handle any user prompt. + self.handle_any_user_prompts(self.session()?.webview_id)?; + let (sender, receiver) = ipc::channel().unwrap(); let cmd = WebDriverScriptCommand::ExecuteScript(script, sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; @@ -1991,22 +1987,11 @@ impl Handler { fn handle_execute_async_script( &self, - parameters: &JavascriptCommandParameters, + parameters: JavascriptCommandParameters, ) -> WebDriverResult { - // 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.session()?.browsing_context_id)?; - // Step 3. Handle any user prompt. - self.handle_any_user_prompts(self.session()?.webview_id)?; - - let func_body = ¶meters.script; - let mut args_string: Vec<_> = parameters - .args - .as_deref() - .unwrap_or(&[]) - .iter() - .map(webdriver_value_to_js_argument) - .collect(); + // Step 1. Let body and arguments be the result of trying to extract the script arguments + // from a request with argument parameters. + let (func_body, mut args_string) = self.extract_script_arguments(parameters)?; args_string.push("resolve".to_string()); let timeout_script = if let Some(script_timeout) = self.session()?.timeouts.script { @@ -2032,6 +2017,13 @@ impl Handler { ); 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.session()?.browsing_context_id)?; + + // Step 3. Handle any user prompt. + self.handle_any_user_prompts(self.session()?.webview_id)?; + let (sender, receiver) = ipc::channel().unwrap(); let cmd = WebDriverScriptCommand::ExecuteAsyncScript(script, sender); self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?; @@ -2524,8 +2516,8 @@ impl WebDriverHandler for Handler { self.handle_perform_actions(actions_parameters) }, WebDriverCommand::ReleaseActions => self.handle_release_actions(), - WebDriverCommand::ExecuteScript(ref x) => self.handle_execute_script(x), - WebDriverCommand::ExecuteAsyncScript(ref x) => self.handle_execute_async_script(x), + 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) }, @@ -2560,54 +2552,6 @@ impl WebDriverHandler for Handler { } } -/// -const ELEMENT_IDENTIFIER: &str = "element-6066-11e4-a52e-4f735466cecf"; -/// -const FRAME_IDENTIFIER: &str = "frame-075b-4da1-b6ba-e579c2d3230a"; -/// -const WINDOW_IDENTIFIER: &str = "window-fcc6-11e5-b4f8-330a88ab9d7f"; -/// -const SHADOW_ROOT_IDENTIFIER: &str = "shadow-6066-11e4-a52e-4f735466cecf"; - -fn webdriver_value_to_js_argument(v: &Value) -> String { - match v { - Value::String(s) => format!("\"{}\"", s), - Value::Null => "null".to_string(), - Value::Bool(b) => b.to_string(), - Value::Number(n) => n.to_string(), - Value::Array(list) => { - let elems = list - .iter() - .map(|v| webdriver_value_to_js_argument(v).to_string()) - .collect::>(); - format!("[{}]", elems.join(", ")) - }, - Value::Object(map) => { - let key = map.keys().next().map(String::as_str); - match (key, map.values().next()) { - (Some(ELEMENT_IDENTIFIER), Some(id)) => { - return format!("window.webdriverElement({})", id); - }, - (Some(FRAME_IDENTIFIER), Some(id)) => { - return format!("window.webdriverFrame({})", id); - }, - (Some(WINDOW_IDENTIFIER), Some(id)) => { - return format!("window.webdriverWindow({})", id); - }, - (Some(SHADOW_ROOT_IDENTIFIER), Some(id)) => { - return format!("window.webdriverShadowRoot({})", id); - }, - _ => {}, - } - let elems = map - .iter() - .map(|(k, v)| format!("{}: {}", k, webdriver_value_to_js_argument(v))) - .collect::>(); - format!("{{{}}}", elems.join(", ")) - }, - } -} - fn wait_for_ipc_response(receiver: IpcReceiver) -> Result where T: for<'de> Deserialize<'de> + Serialize, diff --git a/tests/wpt/meta/webdriver/tests/classic/execute_async_script/arguments.py.ini b/tests/wpt/meta/webdriver/tests/classic/execute_async_script/arguments.py.ini index 72a20a6f7cf..ec1cbb50358 100644 --- a/tests/wpt/meta/webdriver/tests/classic/execute_async_script/arguments.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/execute_async_script/arguments.py.ini @@ -1,40 +1,4 @@ [arguments.py] - [test_no_such_element_with_unknown_id] - expected: FAIL - - [test_no_such_element_from_other_window_handle[open\]] - expected: FAIL - - [test_no_such_element_from_other_window_handle[closed\]] - expected: FAIL - - [test_no_such_element_from_other_frame[open\]] - expected: FAIL - - [test_no_such_element_from_other_frame[closed\]] - expected: FAIL - - [test_no_such_shadow_root_with_unknown_id] - expected: FAIL - - [test_no_such_shadow_root_from_other_window_handle[open\]] - expected: FAIL - - [test_no_such_shadow_root_from_other_window_handle[closed\]] - expected: FAIL - - [test_no_such_shadow_root_from_other_frame[open\]] - expected: FAIL - - [test_no_such_shadow_root_from_other_frame[closed\]] - expected: FAIL - - [test_stale_element_reference[top_context\]] - expected: FAIL - - [test_stale_element_reference[child_context\]] - expected: FAIL - [test_invalid_argument_for_window_with_invalid_type[None-frame\]] expected: FAIL @@ -73,6 +37,3 @@ [test_element_reference[shadow-root\]] expected: FAIL - - [test_element_reference[window\]] - expected: FAIL diff --git a/tests/wpt/meta/webdriver/tests/classic/execute_script/arguments.py.ini b/tests/wpt/meta/webdriver/tests/classic/execute_script/arguments.py.ini index 9c6615f080f..ec1cbb50358 100644 --- a/tests/wpt/meta/webdriver/tests/classic/execute_script/arguments.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/execute_script/arguments.py.ini @@ -1,46 +1,4 @@ [arguments.py] - [test_no_such_element_with_unknown_id] - expected: FAIL - - [test_no_such_element_from_other_window_handle[open\]] - expected: FAIL - - [test_no_such_element_from_other_window_handle[closed\]] - expected: FAIL - - [test_no_such_element_from_other_frame[open\]] - expected: FAIL - - [test_no_such_element_from_other_frame[closed\]] - expected: FAIL - - [test_no_such_shadow_root_with_unknown_id] - expected: FAIL - - [test_no_such_shadow_root_from_other_window_handle[open\]] - expected: FAIL - - [test_no_such_shadow_root_from_other_window_handle[closed\]] - expected: FAIL - - [test_no_such_shadow_root_from_other_frame[open\]] - expected: FAIL - - [test_no_such_shadow_root_from_other_frame[closed\]] - expected: FAIL - - [test_detached_shadow_root_reference[top_context\]] - expected: FAIL - - [test_detached_shadow_root_reference[child_context\]] - expected: FAIL - - [test_stale_element_reference[top_context\]] - expected: FAIL - - [test_stale_element_reference[child_context\]] - expected: FAIL - [test_invalid_argument_for_window_with_invalid_type[None-frame\]] expected: FAIL @@ -79,6 +37,3 @@ [test_element_reference[shadow-root\]] expected: FAIL - - [test_element_reference[window\]] - expected: FAIL