use serde_json::Value; use super::cdp::client::CdpClient; use super::cdp::types::*; use super::element::{resolve_element_center, resolve_element_object_id, RefMap}; pub async fn click( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, button: &str, click_count: i32, ) -> Result<(), String> { let (x, y) = resolve_element_center(client, session_id, ref_map, selector_or_ref).await?; dispatch_click(client, session_id, x, y, button, click_count).await } pub async fn dblclick( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, ) -> Result<(), String> { click(client, session_id, ref_map, selector_or_ref, "left", 2).await } pub async fn hover( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, ) -> Result<(), String> { let (x, y) = resolve_element_center(client, session_id, ref_map, selector_or_ref).await?; client .send_command_typed::<_, Value>( "Input.dispatchMouseEvent", &DispatchMouseEventParams { event_type: "mouseMoved".to_string(), x, y, button: None, buttons: None, click_count: None, delta_x: None, delta_y: None, modifiers: None, }, Some(session_id), ) .await?; Ok(()) } pub async fn fill( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, value: &str, ) -> Result<(), String> { let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?; // Focus the element client .send_command_typed::<_, Value>( "Runtime.callFunctionOn", &CallFunctionOnParams { function_declaration: "function() { this.focus(); }".to_string(), object_id: Some(object_id.clone()), arguments: None, return_by_value: Some(true), await_promise: Some(false), }, Some(session_id), ) .await?; // Select all + delete to clear client .send_command_typed::<_, Value>( "Runtime.callFunctionOn", &CallFunctionOnParams { function_declaration: r#"function() { this.select && this.select(); this.value = ''; this.dispatchEvent(new Event('input', { bubbles: true })); }"# .to_string(), object_id: Some(object_id), arguments: None, return_by_value: Some(true), await_promise: Some(false), }, Some(session_id), ) .await?; // Insert text client .send_command_typed::<_, Value>( "Input.insertText", &InsertTextParams { text: value.to_string(), }, Some(session_id), ) .await?; Ok(()) } pub async fn type_text( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, text: &str, clear: bool, delay_ms: Option, ) -> Result<(), String> { let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?; // Focus client .send_command_typed::<_, Value>( "Runtime.callFunctionOn", &CallFunctionOnParams { function_declaration: "function() { this.focus(); }".to_string(), object_id: Some(object_id.clone()), arguments: None, return_by_value: Some(true), await_promise: Some(false), }, Some(session_id), ) .await?; if clear { client .send_command_typed::<_, Value>( "Runtime.callFunctionOn", &CallFunctionOnParams { function_declaration: r#"function() { this.select && this.select(); this.value = ''; this.dispatchEvent(new Event('input', { bubbles: true })); }"# .to_string(), object_id: Some(object_id), arguments: None, return_by_value: Some(true), await_promise: Some(false), }, Some(session_id), ) .await?; } let delay = delay_ms.unwrap_or(0); for ch in text.chars() { let text_str = ch.to_string(); let (key, code, key_code) = char_to_key_info(ch); // Characters that have no US-keyboard mapping (key_code == 0 and empty // code) are inserted via `Input.insertText`, matching Playwright's // keyboard.type() fallback behaviour. This handles emoji, CJK, and // other characters that don't correspond to a physical key. if key_code == 0 && code.is_empty() { client .send_command_typed::<_, Value>( "Input.insertText", &InsertTextParams { text: text_str }, Some(session_id), ) .await?; } else { client .send_command_typed::<_, Value>( "Input.dispatchKeyEvent", &DispatchKeyEventParams { event_type: "keyDown".to_string(), key: Some(key.clone()), code: Some(code.clone()), text: Some(text_str.clone()), unmodified_text: Some(text_str.clone()), windows_virtual_key_code: Some(key_code), native_virtual_key_code: Some(key_code), modifiers: None, }, Some(session_id), ) .await?; client .send_command_typed::<_, Value>( "Input.dispatchKeyEvent", &DispatchKeyEventParams { event_type: "keyUp".to_string(), key: Some(key), code: Some(code), text: None, unmodified_text: None, windows_virtual_key_code: Some(key_code), native_virtual_key_code: Some(key_code), modifiers: None, }, Some(session_id), ) .await?; } if delay > 0 { tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await; } } Ok(()) } pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Result<(), String> { press_key_with_modifiers(client, session_id, key, None).await } /// Dispatch a keyDown+keyUp sequence for `key` with an optional CDP modifier bitmask. /// /// Modifier values follow the CDP `Input.dispatchKeyEvent` spec: /// 1 = Alt, 2 = Control, 4 = Meta (Cmd), 8 = Shift. /// /// Callers that need a platform-appropriate modifier (e.g. Cmd on macOS, /// Ctrl elsewhere) must choose the value themselves -- see `cfg!(target_os)`. pub async fn press_key_with_modifiers( client: &CdpClient, session_id: &str, key: &str, modifiers: Option, ) -> Result<(), String> { let (key_name, code, key_code) = named_key_info(key); client .send_command_typed::<_, Value>( "Input.dispatchKeyEvent", &DispatchKeyEventParams { event_type: "keyDown".to_string(), key: Some(key_name.clone()), code: Some(code.clone()), text: None, unmodified_text: None, windows_virtual_key_code: Some(key_code), native_virtual_key_code: Some(key_code), modifiers, }, Some(session_id), ) .await?; client .send_command_typed::<_, Value>( "Input.dispatchKeyEvent", &DispatchKeyEventParams { event_type: "keyUp".to_string(), key: Some(key_name), code: Some(code), text: None, unmodified_text: None, windows_virtual_key_code: Some(key_code), native_virtual_key_code: Some(key_code), modifiers, }, Some(session_id), ) .await?; Ok(()) } pub async fn scroll( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: Option<&str>, delta_x: f64, delta_y: f64, ) -> Result<(), String> { if let Some(sel) = selector_or_ref { let object_id = resolve_element_object_id(client, session_id, ref_map, sel).await?; let js = "function(dx, dy) { this.scrollBy(dx, dy); }".to_string(); client .send_command_typed::<_, Value>( "Runtime.callFunctionOn", &CallFunctionOnParams { function_declaration: js, object_id: Some(object_id), arguments: Some(vec![ CallArgument { value: Some(serde_json::json!(delta_x)), object_id: None, }, CallArgument { value: Some(serde_json::json!(delta_y)), object_id: None, }, ]), return_by_value: Some(true), await_promise: Some(false), }, Some(session_id), ) .await?; } else { let js = format!("window.scrollBy({}, {})", delta_x, delta_y); client .send_command_typed::<_, Value>( "Runtime.evaluate", &EvaluateParams { expression: js, return_by_value: Some(true), await_promise: Some(false), }, Some(session_id), ) .await?; } Ok(()) } pub async fn select_option( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, values: &[String], ) -> Result<(), String> { let object_id = resolve_element_object_id(client, session_id, ref_map, selector_or_ref).await?; let js = r#"function(vals) { const options = Array.from(this.options); for (const opt of options) { opt.selected = vals.includes(opt.value) || vals.includes(opt.textContent.trim()); } this.dispatchEvent(new Event('change', { bubbles: true })); }"# .to_string(); client .send_command_typed::<_, Value>( "Runtime.callFunctionOn", &CallFunctionOnParams { function_declaration: js, object_id: Some(object_id), arguments: Some(vec![CallArgument { value: Some(serde_json::json!(values)), object_id: None, }]), return_by_value: Some(true), await_promise: Some(false), }, Some(session_id), ) .await?; Ok(()) } pub async fn check( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, ) -> Result<(), String> { let is_checked = super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await?; if !is_checked { click(client, session_id, ref_map, selector_or_ref, "left", 1).await?; // Verify the click changed the state (Playwright parity: _setChecked re-checks). // If the coordinate-based click missed (e.g. hidden input, overlay), retry // with a JS .click() on the element and its associated input. if !super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await? { js_click_checkbox(client, session_id, ref_map, selector_or_ref).await?; } } Ok(()) } pub async fn uncheck( client: &CdpClient, session_id: &str, ref_map: &RefMap, selector_or_ref: &str, ) -> Result<(), String> { let is_checked = super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await?; if is_checked { click(client, session_id, ref_map, selector_or_ref, "left", 1).await?; // Same verify-and-retry as check(). if super::element::is_element_checked(client, session_id, ref_map, selector_or_ref).await? { js_click_checkbox(client, session_id, ref_map, selector_or_ref).await?; } } Ok(()) } /// Fallback for when the coordinate-based CDP click did not toggle the /// checkbox/radio state. This mirrors how Playwright dispatches clicks /// through the DOM rather than via raw Input.dispatchMouseEvent coordinates. /// /// Uses the same follow-label resolution as `is_element_checked`: /// 1. If the element is a native input → `.click()` it directly. /// 2. If the element is inside a `