1use super::{Command, CommandContext, CommandResult};
6use crate::error::CliError;
7use crate::tools::browser::client_ext::{InteractionExt, NavigationExt, QueryExt};
8use crate::tools::browser::{BrowserClient, BrowserConfig};
9use std::sync::Arc;
10use tokio::sync::Mutex;
11
12pub struct BrowserCommand {
14 client: Arc<Mutex<BrowserClient>>,
15}
16
17impl BrowserCommand {
18 pub fn new() -> Self {
20 let client = BrowserClient::with_default_config();
21 Self {
22 client: Arc::new(Mutex::new(client)),
23 }
24 }
25
26 pub fn with_config(config: BrowserConfig) -> Self {
28 use crate::tools::browser::executor::CliExecutor;
29 let executor = Arc::new(CliExecutor::new(config));
30 let client = BrowserClient::new(executor);
31 Self {
32 client: Arc::new(Mutex::new(client)),
33 }
34 }
35
36 fn show_help(&self, ctx: &mut CommandContext) -> CommandResult {
38 let help_text = r#"Browser automation commands:
39
40Navigation:
41 /browser open <url> Open a URL in the browser
42 /browser back Navigate back in history
43 /browser forward Navigate forward in history
44 /browser reload Reload the current page
45
46Page Interaction:
47 /browser snapshot Take an accessibility snapshot
48 /browser click <selector> Click an element
49 /browser fill <sel> <text> Fill a form field (instant)
50 /browser type <sel> <text> Type text character by character
51 /browser hover <selector> Hover over an element
52 /browser select <sel> <val> Select option in dropdown
53 /browser press <key> Press a keyboard key
54
55State & Info:
56 /browser screenshot <path> Save a screenshot
57 /browser get <what> Get page content (text, html, url, title)
58 /browser scroll <dir> [px] Scroll page (up/down/left/right)
59 /browser is <what> <sel> Check element state (visible, enabled, etc.)
60 /browser close Close the browser
61 /browser help Show this help
62
63Examples:
64 /browser open https://example.com
65 /browser snapshot
66 /browser click "button.submit"
67 /browser fill "input[name=email]" "test@example.com"
68 /browser type "input[name=password]" "secret"
69 /browser hover "@e5"
70 /browser press Enter
71 /browser scroll down 100
72 /browser is visible "@e3"
73 /browser screenshot /tmp/page.png
74 /browser get title"#;
75
76 ctx.add_system_message(help_text.to_string());
77 CommandResult::Continue
78 }
79
80 fn parse_args(&self, input: &str) -> Vec<String> {
82 let mut args = Vec::new();
84 let mut current = String::new();
85 let mut in_quotes = false;
86 let mut quote_char = ' ';
87
88 for ch in input.chars() {
89 match ch {
90 '"' | '\'' if !in_quotes => {
91 in_quotes = true;
92 quote_char = ch;
93 }
94 c if c == quote_char && in_quotes => {
95 in_quotes = false;
96 quote_char = ' ';
97 }
98 ' ' if !in_quotes => {
99 if !current.is_empty() {
100 args.push(current.clone());
101 current.clear();
102 }
103 }
104 _ => {
105 current.push(ch);
106 }
107 }
108 }
109
110 if !current.is_empty() {
111 args.push(current);
112 }
113
114 args
115 }
116}
117
118impl Default for BrowserCommand {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl Command for BrowserCommand {
125 fn name(&self) -> &str {
126 "browser"
127 }
128
129 fn aliases(&self) -> Vec<&str> {
130 vec!["b"]
131 }
132
133 fn description(&self) -> &str {
134 "Browser automation for testing, scraping, and screenshots"
135 }
136
137 fn usage(&self) -> Vec<&str> {
138 vec![
139 "/browser open <url>",
140 "/browser close",
141 "/browser snapshot",
142 "/browser click <selector>",
143 "/browser fill <selector> <text>",
144 "/browser type <selector> <text>",
145 "/browser hover <selector>",
146 "/browser press <key>",
147 "/browser select <selector> <value>",
148 "/browser screenshot <path>",
149 "/browser get <text|html|url|title>",
150 "/browser back",
151 "/browser forward",
152 "/browser reload",
153 "/browser scroll <up|down|left|right> [pixels]",
154 "/browser is <visible|hidden|enabled|disabled|editable> <selector>",
155 ]
156 }
157
158 fn execute(&self, args: &str, ctx: &mut CommandContext) -> Result<CommandResult, CliError> {
159 let args = args.trim();
160
161 if args.is_empty() || args == "help" {
163 return Ok(self.show_help(ctx));
164 }
165
166 let parsed = self.parse_args(args);
167 if parsed.is_empty() {
168 return Ok(self.show_help(ctx));
169 }
170
171 let action = parsed[0].to_lowercase();
172 let client = self.client.clone();
173
174 let rt = tokio::runtime::Runtime::new()
176 .map_err(|e| CliError::Other(format!("Failed to create runtime: {}", e)))?;
177
178 let result = rt.block_on(async {
179 let client = client.lock().await;
180
181 match action.as_str() {
182 "open" => {
183 if parsed.len() < 2 {
184 return Err(CliError::Other("Usage: /browser open <url>".to_string()));
185 }
186 let url = &parsed[1];
187 client
188 .open(url)
189 .await
190 .map_err(|e| CliError::Other(format!("Failed to open URL: {}", e)))?;
191 ctx.add_system_message(format!("Opened: {}", url));
192 Ok(CommandResult::Continue)
193 }
194
195 "close" => {
196 client
197 .close()
198 .await
199 .map_err(|e| CliError::Other(format!("Failed to close browser: {}", e)))?;
200 ctx.add_system_message("Browser closed".to_string());
201 Ok(CommandResult::Continue)
202 }
203
204 "snapshot" => {
205 let result = client
206 .snapshot()
207 .await
208 .map_err(|e| CliError::Other(format!("Failed to take snapshot: {}", e)))?;
209
210 let mut msg = String::new();
211 if let Some(title) = &result.title {
212 msg.push_str(&format!("Title: {}\n", title));
213 }
214 if let Some(url) = &result.url {
215 msg.push_str(&format!("URL: {}\n", url));
216 }
217 msg.push_str("\n--- Snapshot ---\n");
218 msg.push_str(&result.content);
219
220 ctx.add_system_message(msg);
221 Ok(CommandResult::Continue)
222 }
223
224 "click" => {
225 if parsed.len() < 2 {
226 return Err(CliError::Other(
227 "Usage: /browser click <selector>".to_string(),
228 ));
229 }
230 let selector = &parsed[1];
231 client
232 .click(selector)
233 .await
234 .map_err(|e| CliError::Other(format!("Failed to click: {}", e)))?;
235 ctx.add_system_message(format!("Clicked: {}", selector));
236 Ok(CommandResult::Continue)
237 }
238
239 "fill" => {
240 if parsed.len() < 3 {
241 return Err(CliError::Other(
242 "Usage: /browser fill <selector> <text>".to_string(),
243 ));
244 }
245 let selector = &parsed[1];
246 let text = &parsed[2];
247 client
248 .fill(selector, text)
249 .await
250 .map_err(|e| CliError::Other(format!("Failed to fill: {}", e)))?;
251 ctx.add_system_message(format!("Filled {} with text", selector));
252 Ok(CommandResult::Continue)
253 }
254
255 "screenshot" => {
256 if parsed.len() < 2 {
257 return Err(CliError::Other(
258 "Usage: /browser screenshot <path>".to_string(),
259 ));
260 }
261 let path = &parsed[1];
262 client.screenshot(path).await.map_err(|e| {
263 CliError::Other(format!("Failed to take screenshot: {}", e))
264 })?;
265 ctx.add_system_message(format!("Screenshot saved to: {}", path));
266 Ok(CommandResult::Continue)
267 }
268
269 "get" => {
270 if parsed.len() < 2 {
271 return Err(CliError::Other(
272 "Usage: /browser get <text|html|url|title>".to_string(),
273 ));
274 }
275 let what = &parsed[1];
276 let content = client
277 .get(what)
278 .await
279 .map_err(|e| CliError::Other(format!("Failed to get {}: {}", what, e)))?;
280 ctx.add_system_message(format!("{}: {}", what, content));
281 Ok(CommandResult::Continue)
282 }
283
284 "back" => {
286 client
287 .back()
288 .await
289 .map_err(|e| CliError::Other(format!("Failed to navigate back: {}", e)))?;
290 ctx.add_system_message("Navigated back".to_string());
291 Ok(CommandResult::Continue)
292 }
293
294 "forward" => {
295 client
296 .forward()
297 .await
298 .map_err(|e| CliError::Other(format!("Failed to navigate forward: {}", e)))?;
299 ctx.add_system_message("Navigated forward".to_string());
300 Ok(CommandResult::Continue)
301 }
302
303 "reload" => {
304 client
305 .reload()
306 .await
307 .map_err(|e| CliError::Other(format!("Failed to reload: {}", e)))?;
308 ctx.add_system_message("Page reloaded".to_string());
309 Ok(CommandResult::Continue)
310 }
311
312 "type" => {
314 if parsed.len() < 3 {
315 return Err(CliError::Other(
316 "Usage: /browser type <selector> <text>".to_string(),
317 ));
318 }
319 let selector = &parsed[1];
320 let text = &parsed[2];
321 client
322 .type_text(selector, text)
323 .await
324 .map_err(|e| CliError::Other(format!("Failed to type: {}", e)))?;
325 ctx.add_system_message(format!("Typed text into {}", selector));
326 Ok(CommandResult::Continue)
327 }
328
329 "press" => {
330 if parsed.len() < 2 {
331 return Err(CliError::Other("Usage: /browser press <key>".to_string()));
332 }
333 let key = &parsed[1];
334 client
335 .press(key)
336 .await
337 .map_err(|e| CliError::Other(format!("Failed to press key: {}", e)))?;
338 ctx.add_system_message(format!("Pressed: {}", key));
339 Ok(CommandResult::Continue)
340 }
341
342 "hover" => {
343 if parsed.len() < 2 {
344 return Err(CliError::Other(
345 "Usage: /browser hover <selector>".to_string(),
346 ));
347 }
348 let selector = &parsed[1];
349 client
350 .hover(selector)
351 .await
352 .map_err(|e| CliError::Other(format!("Failed to hover: {}", e)))?;
353 ctx.add_system_message(format!("Hovered over: {}", selector));
354 Ok(CommandResult::Continue)
355 }
356
357 "select" => {
358 if parsed.len() < 3 {
359 return Err(CliError::Other(
360 "Usage: /browser select <selector> <value>".to_string(),
361 ));
362 }
363 let selector = &parsed[1];
364 let value = &parsed[2];
365 client
366 .select_option(selector, value)
367 .await
368 .map_err(|e| CliError::Other(format!("Failed to select: {}", e)))?;
369 ctx.add_system_message(format!("Selected '{}' in {}", value, selector));
370 Ok(CommandResult::Continue)
371 }
372
373 "scroll" => {
375 if parsed.len() < 2 {
376 return Err(CliError::Other(
377 "Usage: /browser scroll <up|down|left|right> [pixels]".to_string(),
378 ));
379 }
380 let direction = &parsed[1];
381 let pixels = parsed.get(2).and_then(|s| s.parse::<u32>().ok());
382 client
383 .scroll(direction, pixels)
384 .await
385 .map_err(|e| CliError::Other(format!("Failed to scroll: {}", e)))?;
386 ctx.add_system_message(format!("Scrolled {}", direction));
387 Ok(CommandResult::Continue)
388 }
389
390 "is" => {
391 if parsed.len() < 3 {
392 return Err(CliError::Other(
393 "Usage: /browser is <visible|hidden|enabled|disabled|editable> <selector>".to_string(),
394 ));
395 }
396 let what = &parsed[1];
397 let selector = &parsed[2];
398 let result = client
399 .is_(what, selector)
400 .await
401 .map_err(|e| CliError::Other(format!("Failed to check state: {}", e)))?;
402 ctx.add_system_message(format!("Element {} is {}: {}", selector, what, result));
403 Ok(CommandResult::Continue)
404 }
405
406 _ => {
407 ctx.add_system_message(format!("Unknown browser action: {}", action));
408 ctx.add_system_message("Type /browser help for available commands".to_string());
409 Ok(CommandResult::Continue)
410 }
411 }
412 });
413
414 result
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_browser_command_name() {
424 let cmd = BrowserCommand::new();
425 assert_eq!(cmd.name(), "browser");
426 }
427
428 #[test]
429 fn test_browser_command_aliases() {
430 let cmd = BrowserCommand::new();
431 assert_eq!(cmd.aliases(), vec!["b"]);
432 }
433
434 #[test]
435 fn test_browser_command_description() {
436 let cmd = BrowserCommand::new();
437 assert!(!cmd.description().is_empty());
438 }
439
440 #[test]
441 fn test_parse_args_simple() {
442 let cmd = BrowserCommand::new();
443 let args = cmd.parse_args("open https://example.com");
444 assert_eq!(args, vec!["open", "https://example.com"]);
445 }
446
447 #[test]
448 fn test_parse_args_quoted() {
449 let cmd = BrowserCommand::new();
450 let args = cmd.parse_args("fill 'input[name=email]' \"test text\"");
451 assert_eq!(args, vec!["fill", "input[name=email]", "test text"]);
452 }
453
454 #[test]
455 fn test_parse_args_empty() {
456 let cmd = BrowserCommand::new();
457 let args = cmd.parse_args("");
458 assert!(args.is_empty());
459 }
460}