#![cfg(feature = "browser")]
use crate::brain::tools::browser::{Act, build_pre_flight_js, format_prefix_report, parse_actions};
use serde_json::json;
fn actions(v: serde_json::Value) -> serde_json::Value {
json!({ "actions": v })
}
#[test]
fn parses_all_five_action_types() {
let input = actions(json!([
{"type": "click", "selector": "#submit"},
{"type": "fill", "selector": "input[name=user]", "text": "adolfo"},
{"type": "press", "key": "Enter"},
{"type": "select", "selector": "#country", "value": "PT"},
{"type": "wait", "ms": 500},
]));
let acts = parse_actions(&input).expect("valid batch parses");
assert_eq!(acts.len(), 5);
assert_eq!(
acts[0],
Act::Click {
selector: "#submit".into()
}
);
assert_eq!(
acts[1],
Act::Fill {
selector: "input[name=user]".into(),
text: "adolfo".into()
}
);
assert_eq!(
acts[2],
Act::Press {
key: "Enter".into()
}
);
assert_eq!(
acts[3],
Act::Select {
selector: "#country".into(),
value: "PT".into()
}
);
assert_eq!(acts[4], Act::Wait { ms: 500 });
}
#[test]
fn cap_of_ten_is_enforced_up_front() {
let eleven: Vec<_> = (0..11)
.map(|i| json!({"type": "press", "key": format!("Key{i}")}))
.collect();
let err = parse_actions(&actions(json!(eleven))).unwrap_err();
assert!(err.contains("Too many actions: 11"), "got: {err}");
assert!(err.contains("cap is 10"), "got: {err}");
let ten: Vec<_> = (0..10)
.map(|i| json!({"type": "press", "key": format!("Key{i}")}))
.collect();
assert_eq!(parse_actions(&actions(json!(ten))).unwrap().len(), 10);
}
#[test]
fn unknown_type_and_missing_fields_are_rejected_with_action_numbers() {
let err = parse_actions(&actions(json!([
{"type": "press", "key": "Enter"},
{"type": "drag", "selector": "#x"},
])))
.unwrap_err();
assert!(err.contains("action 2"), "got: {err}");
assert!(err.contains("unknown type 'drag'"), "got: {err}");
let err = parse_actions(&actions(json!([{"type": "fill", "selector": "#u"}]))).unwrap_err();
assert!(err.contains("action 1"), "got: {err}");
assert!(err.contains("\"text\""), "got: {err}");
let err = parse_actions(&actions(json!([{"type": "click"}]))).unwrap_err();
assert!(err.contains("\"selector\""), "got: {err}");
}
#[test]
fn wait_is_bounded_per_action() {
let err = parse_actions(&actions(json!([{"type": "wait", "ms": 60000}]))).unwrap_err();
assert!(err.contains("exceeds the per-action cap"), "got: {err}");
let err = parse_actions(&actions(json!([{"type": "wait", "ms": 0}]))).unwrap_err();
assert!(err.contains("positive"), "got: {err}");
}
#[test]
fn empty_or_missing_actions_array_is_rejected() {
assert!(parse_actions(&json!({})).is_err());
assert!(parse_actions(&actions(json!([]))).is_err());
}
#[test]
fn prefix_report_names_completed_prefix_and_holds_remaining() {
let failed = Act::Click {
selector: "#gone".into(),
};
let r = format_prefix_report(2, &failed, "element no longer present");
assert!(r.starts_with("actions 1-2 OK;"), "got: {r}");
assert!(r.contains("action 3 (click #gone) failed"), "got: {r}");
assert!(r.contains("NOT run"), "got: {r}");
let r = format_prefix_report(0, &failed, "not found");
assert!(r.contains("action 1 (click #gone) failed"), "got: {r}");
assert!(r.contains("No actions executed"), "got: {r}");
}
#[test]
fn pre_flight_js_embeds_all_selectors_and_all_three_shapes() {
let js = build_pre_flight_js(&[
"#submit".into(),
"text=Sign in".into(),
"xpath=//button[1]".into(),
]);
assert!(js.contains("\"#submit\""), "js: {js}");
assert!(js.contains(r#""text=Sign in""#), "js: {js}");
assert!(js.contains(r#""xpath=//button[1]""#), "js: {js}");
assert!(js.contains("text=") && js.contains("createTreeWalker"));
assert!(js.contains("xpath=") && js.contains("document.evaluate"));
assert!(js.contains("querySelector"));
assert!(js.contains("getBoundingClientRect"));
assert!(js.contains("results.push"));
}
#[tokio::test]
#[ignore = "launches real Chrome — opt-in via `cargo test -- --ignored browser_act`"]
async fn login_form_completes_in_one_call() {
use crate::brain::tools::browser::{BrowserActTool, BrowserManager};
use crate::brain::tools::{Tool, ToolExecutionContext};
use std::sync::Arc;
use uuid::Uuid;
let mgr = Arc::new(BrowserManager::new(Default::default()));
let act = BrowserActTool::new(mgr.clone());
let ctx = ToolExecutionContext::new(Uuid::new_v4());
let nav = crate::brain::tools::browser::BrowserNavigateTool::new(mgr.clone());
let _ = Tool::execute(
&nav,
json!({"url": "https://the-internet.herokuapp.com/login"}),
&ctx,
)
.await;
let out = Tool::execute(
&act,
actions(json!([
{"type": "fill", "selector": "#username", "text": "tomsmith"},
{"type": "fill", "selector": "#password", "text": "SuperSecretPassword!"},
{"type": "press", "key": "Enter"},
])),
&ctx,
)
.await
.expect("execute ok");
assert!(out.success, "batch should succeed: {:?}", out.error);
assert!(
out.output.contains("Executed 3 actions"),
"got: {}",
out.output
);
let close = crate::brain::tools::browser::BrowserCloseTool::new(mgr);
let _ = close.execute(serde_json::json!({}), &ctx).await;
}