diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 9b188fb..8a77a74 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -4472,7 +4472,8 @@ async fn handle_upload(cmd: &Value, state: &DaemonState) -> Result Result<(), String> { + pub async fn upload_files( + &self, + selector: &str, + files: &[String], + ref_map: &RefMap, + iframe_sessions: &HashMap, + ) -> Result<(), String> { let session_id = self.active_session_id()?; - let node_result = self - .client - .send_command( - "DOM.querySelector", - Some(json!({ - "nodeId": 1, - "selector": selector, - })), - Some(session_id), - ) - .await; + let (object_id, effective_session_id) = + resolve_element_object_id(&self.client, session_id, ref_map, selector, iframe_sessions) + .await?; - // Alternative: resolve via JS - let result: EvaluateResult = self - .client - .send_command_typed( - "Runtime.evaluate", - &EvaluateParams { - expression: format!( - "document.querySelector({})", - serde_json::to_string(selector).unwrap_or_default() - ), - return_by_value: Some(false), - await_promise: Some(false), - }, - Some(session_id), - ) - .await?; - - let object_id = result - .result - .object_id - .ok_or("File input element not found")?; - - // Get the DOM node from the remote object let describe: Value = self .client .send_command( "DOM.describeNode", Some(json!({ "objectId": object_id })), - Some(session_id), + Some(&effective_session_id), ) .await?; @@ -1144,9 +1120,6 @@ impl BrowserManager { .and_then(|v| v.as_i64()) .ok_or("Could not get backendNodeId for file input")?; - // Suppress unused variable warning - let _ = node_result; - self.client .send_command( "DOM.setFileInputFiles", @@ -1154,7 +1127,7 @@ impl BrowserManager { "files": files, "backendNodeId": backend_node_id, })), - Some(session_id), + Some(&effective_session_id), ) .await?; diff --git a/cli/src/native/e2e_tests.rs b/cli/src/native/e2e_tests.rs index 56bfb54..dcccc3b 100644 --- a/cli/src/native/e2e_tests.rs +++ b/cli/src/native/e2e_tests.rs @@ -34,6 +34,7 @@ fn native_test_fixture_html(name: &str) -> &'static str { "drag_probe" => include_str!("test_fixtures/drag_probe.html"), "html5_drag_probe" => include_str!("test_fixtures/html5_drag_probe.html"), "pointer_capture_probe" => include_str!("test_fixtures/pointer_capture_probe.html"), + "upload_probe" => include_str!("test_fixtures/upload_probe.html"), _ => panic!("Unknown native test fixture: {}", name), } } @@ -3884,3 +3885,96 @@ async fn e2e_relaunch_on_options_change() { let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await; assert_success(&resp); } + +// --------------------------------------------------------------------------- +// Upload: ref-based selector support (issue #1107) +// --------------------------------------------------------------------------- + +#[tokio::test] +#[ignore] +async fn e2e_upload_with_ref_selector() { + let mut state = DaemonState::new(); + + let resp = execute_command( + &json!({ "id": "1", "action": "launch", "headless": true }), + &mut state, + ) + .await; + assert_success(&resp); + + let resp = execute_command( + &json!({ "id": "2", "action": "navigate", "url": native_test_fixture_url("upload_probe") }), + &mut state, + ) + .await; + assert_success(&resp); + + let resp = execute_command(&json!({ "id": "3", "action": "snapshot" }), &mut state).await; + assert_success(&resp); + let snapshot = get_data(&resp)["snapshot"].as_str().unwrap(); + + // Match by label text, not by role which may vary across Chrome versions + let file_input_ref = snapshot + .lines() + .filter_map(|line| { + if line.contains("Choose file") && line.contains("ref=") { + let start = line.find("ref=")? + 4; + let end = line[start..].find(']')? + start; + Some(line[start..end].to_string()) + } else { + None + } + }) + .next() + .expect("Snapshot should contain the file input with a ref"); + + let tmp = std::env::temp_dir().join(format!("ab-upload-ref-{}.txt", std::process::id())); + std::fs::write(&tmp, "test").unwrap(); + + let resp = execute_command( + &json!({ "id": "4", "action": "upload", "selector": file_input_ref, "files": [tmp.to_string_lossy()] }), + &mut state, + ) + .await; + assert_success(&resp); + assert_eq!(get_data(&resp)["uploaded"], 1); + + let _ = std::fs::remove_file(&tmp); + let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await; + assert_success(&resp); +} + +#[tokio::test] +#[ignore] +async fn e2e_upload_with_css_selector() { + let mut state = DaemonState::new(); + + let resp = execute_command( + &json!({ "id": "1", "action": "launch", "headless": true }), + &mut state, + ) + .await; + assert_success(&resp); + + let resp = execute_command( + &json!({ "id": "2", "action": "navigate", "url": native_test_fixture_url("upload_probe") }), + &mut state, + ) + .await; + assert_success(&resp); + + let tmp = std::env::temp_dir().join(format!("ab-upload-css-{}.txt", std::process::id())); + std::fs::write(&tmp, "test").unwrap(); + + let resp = execute_command( + &json!({ "id": "3", "action": "upload", "selector": "#fileInput", "files": [tmp.to_string_lossy()] }), + &mut state, + ) + .await; + assert_success(&resp); + assert_eq!(get_data(&resp)["uploaded"], 1); + + let _ = std::fs::remove_file(&tmp); + let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await; + assert_success(&resp); +} diff --git a/cli/src/native/test_fixtures/upload_probe.html b/cli/src/native/test_fixtures/upload_probe.html new file mode 100644 index 0000000..361f366 --- /dev/null +++ b/cli/src/native/test_fixtures/upload_probe.html @@ -0,0 +1,18 @@ + + +Upload Test + +

Upload Test

+ + +
+ + +