diff --git a/README.md b/README.md index 09055c1..62121e0 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ agent-browser mouse wheel [dx] # Scroll wheel ### Browser Settings ```bash -agent-browser set viewport # Set viewport size +agent-browser set viewport [scale] # Set viewport size (scale for retina, e.g. 2) agent-browser set device # Emulate device ("iPhone 14") agent-browser set geo # Set geolocation agent-browser set offline [on|off] # Toggle offline mode diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 2e707ee..bda3eac 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -1871,25 +1871,35 @@ fn parse_set(rest: &[&str], id: &str) -> Result { Some("viewport") => { let w_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments { context: "set viewport".to_string(), - usage: "set viewport ", + usage: "set viewport [scale]", })?; let h_str = rest.get(2).ok_or_else(|| ParseError::MissingArguments { context: "set viewport".to_string(), - usage: "set viewport ", + usage: "set viewport [scale]", })?; let w = w_str .parse::() .map_err(|_| ParseError::MissingArguments { context: "set viewport".to_string(), - usage: "set viewport ", + usage: "set viewport [scale]", })?; let h = h_str .parse::() .map_err(|_| ParseError::MissingArguments { context: "set viewport".to_string(), - usage: "set viewport ", + usage: "set viewport [scale]", })?; - Ok(json!({ "id": id, "action": "viewport", "width": w, "height": h })) + let mut cmd = json!({ "id": id, "action": "viewport", "width": w, "height": h }); + if let Some(scale_str) = rest.get(3) { + let scale = scale_str + .parse::() + .map_err(|_| ParseError::MissingArguments { + context: "set viewport".to_string(), + usage: "set viewport [scale]", + })?; + cmd["deviceScaleFactor"] = json!(scale); + } + Ok(cmd) } Some("device") => { let dev = rest.get(1).ok_or_else(|| ParseError::MissingArguments { @@ -3045,6 +3055,45 @@ mod tests { assert_eq!(cmd["reducedMotion"], "reduce"); } + #[test] + fn test_set_viewport() { + let cmd = parse_command(&args("set viewport 1920 1080"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "viewport"); + assert_eq!(cmd["width"], 1920); + assert_eq!(cmd["height"], 1080); + assert!(cmd.get("deviceScaleFactor").is_none()); + } + + #[test] + fn test_set_viewport_with_scale() { + let cmd = parse_command(&args("set viewport 1920 1080 2"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "viewport"); + assert_eq!(cmd["width"], 1920); + assert_eq!(cmd["height"], 1080); + assert_eq!(cmd["deviceScaleFactor"], 2.0); + } + + #[test] + fn test_set_viewport_with_fractional_scale() { + let cmd = parse_command(&args("set viewport 375 812 3"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "viewport"); + assert_eq!(cmd["width"], 375); + assert_eq!(cmd["height"], 812); + assert_eq!(cmd["deviceScaleFactor"], 3.0); + } + + #[test] + fn test_set_viewport_missing_height() { + let result = parse_command(&args("set viewport 1920"), &default_flags()); + assert!(result.is_err()); + } + + #[test] + fn test_set_viewport_invalid_scale() { + let result = parse_command(&args("set viewport 1920 1080 abc"), &default_flags()); + assert!(result.is_err()); + } + #[test] fn test_find_first_no_value() { let cmd = parse_command(&args("find first a click"), &default_flags()).unwrap(); diff --git a/cli/src/native/e2e_tests.rs b/cli/src/native/e2e_tests.rs index eb4820d..d448b40 100644 --- a/cli/src/native/e2e_tests.rs +++ b/cli/src/native/e2e_tests.rs @@ -777,6 +777,77 @@ async fn e2e_wait() { assert_success(&resp); } +// --------------------------------------------------------------------------- +// Viewport with deviceScaleFactor (retina) +// --------------------------------------------------------------------------- + +#[tokio::test] +#[ignore] +async fn e2e_viewport_scale_factor() { + 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": "about:blank" }), + &mut state, + ) + .await; + assert_success(&resp); + + // Default devicePixelRatio should be 1 + let resp = execute_command( + &json!({ "id": "3", "action": "evaluate", "script": "window.devicePixelRatio" }), + &mut state, + ) + .await; + assert_success(&resp); + let default_dpr = get_data(&resp)["result"].as_f64().unwrap(); + assert_eq!(default_dpr, 1.0, "Default devicePixelRatio should be 1"); + + // Set viewport with 2x scale factor + let resp = execute_command( + &json!({ "id": "4", "action": "viewport", "width": 1920, "height": 1080, "deviceScaleFactor": 2.0 }), + &mut state, + ) + .await; + assert_success(&resp); + assert_eq!(get_data(&resp)["width"], 1920); + assert_eq!(get_data(&resp)["height"], 1080); + assert_eq!(get_data(&resp)["deviceScaleFactor"], 2.0); + + // devicePixelRatio should now be 2 + let resp = execute_command( + &json!({ "id": "5", "action": "evaluate", "script": "window.devicePixelRatio" }), + &mut state, + ) + .await; + assert_success(&resp); + let new_dpr = get_data(&resp)["result"].as_f64().unwrap(); + assert_eq!( + new_dpr, 2.0, + "devicePixelRatio should be 2 after setting scale factor" + ); + + // CSS viewport width should still be 1920 (not 3840) + let resp = execute_command( + &json!({ "id": "6", "action": "evaluate", "script": "window.innerWidth" }), + &mut state, + ) + .await; + assert_success(&resp); + let css_width = get_data(&resp)["result"].as_i64().unwrap(); + assert_eq!(css_width, 1920, "CSS width should remain 1920 at 2x scale"); + + let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await; + assert_success(&resp); +} + // --------------------------------------------------------------------------- // Viewport and emulation // --------------------------------------------------------------------------- diff --git a/cli/src/output.rs b/cli/src/output.rs index 8ae07cb..131568c 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1590,7 +1590,7 @@ Usage: agent-browser set [args] Configures various browser settings and emulation options. Settings: - viewport Set viewport size + viewport [scale] Set viewport size (scale = deviceScaleFactor, e.g. 2 for retina) device Emulate device (e.g., "iPhone 12") geo Set geolocation offline [on|off] Toggle offline mode @@ -1605,6 +1605,7 @@ Global Options: Examples: agent-browser set viewport 1920 1080 + agent-browser set viewport 1920 1080 2 # 2x retina agent-browser set device "iPhone 12" agent-browser set geo 37.7749 -122.4194 agent-browser set offline on diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index 582876e..acec1fd 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -123,7 +123,7 @@ agent-browser mouse wheel [dx] # Scroll wheel ## Settings ```bash -agent-browser set viewport # Set viewport size +agent-browser set viewport [scale] # Set viewport size (scale for retina, e.g. 2) agent-browser set device # Emulate device ("iPhone 14") agent-browser set geo # Set geolocation agent-browser set offline [on|off] # Toggle offline mode diff --git a/skills/agent-browser/references/commands.md b/skills/agent-browser/references/commands.md index e77196c..33aadb7 100644 --- a/skills/agent-browser/references/commands.md +++ b/skills/agent-browser/references/commands.md @@ -129,6 +129,7 @@ agent-browser find nth 2 "a" hover ```bash agent-browser set viewport 1920 1080 # Set viewport size +agent-browser set viewport 1920 1080 2 # 2x retina (same CSS size, higher res screenshots) agent-browser set device "iPhone 14" # Emulate device agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation) agent-browser set offline on # Toggle offline mode diff --git a/src/actions.ts b/src/actions.ts index 681a33e..7b3100c 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1465,11 +1465,33 @@ async function handleViewport( command: ViewportCommand, browser: BrowserManager ): Promise { - await browser.setViewport(command.width, command.height); - return successResponse(command.id, { + if (command.deviceScaleFactor && command.deviceScaleFactor !== 1) { + await browser.setViewport(command.width, command.height); + await browser.setDeviceScaleFactor( + command.deviceScaleFactor, + command.width, + command.height, + false + ); + } else { + // deviceScaleFactor is 1 or undefined -- clear any previously-set CDP + // Emulation.setDeviceMetricsOverride so stale DPR doesn't persist. + try { + await browser.clearDeviceMetricsOverride(); + } catch { + // Ignore if override was never set + } + await browser.setViewport(command.width, command.height); + } + + const result: Record = { width: command.width, height: command.height, - }); + }; + if (command.deviceScaleFactor !== undefined) { + result.deviceScaleFactor = command.deviceScaleFactor; + } + return successResponse(command.id, result); } async function handleUserAgent( diff --git a/src/protocol.ts b/src/protocol.ts index a7be39d..4b4200a 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -270,6 +270,7 @@ const viewportSchema = baseCommandSchema.extend({ action: z.literal('viewport'), width: z.number().positive(), height: z.number().positive(), + deviceScaleFactor: z.number().positive().optional(), }); const userAgentSchema = baseCommandSchema.extend({ diff --git a/src/types.ts b/src/types.ts index ed9e1f4..4d88f61 100644 --- a/src/types.ts +++ b/src/types.ts @@ -263,6 +263,7 @@ export interface ViewportCommand extends BaseCommand { action: 'viewport'; width: number; height: number; + deviceScaleFactor?: number; } // User agent