add scale factor to set viewport for retina screenshots (#691)
* device scale * fix node.js daemon * fix cargo fmt formatting for scale factor code * fixes
This commit is contained in:
@@ -197,7 +197,7 @@ agent-browser mouse wheel <dy> [dx] # Scroll wheel
|
|||||||
### Browser Settings
|
### Browser Settings
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-browser set viewport <w> <h> # Set viewport size
|
agent-browser set viewport <w> <h> [scale] # Set viewport size (scale for retina, e.g. 2)
|
||||||
agent-browser set device <name> # Emulate device ("iPhone 14")
|
agent-browser set device <name> # Emulate device ("iPhone 14")
|
||||||
agent-browser set geo <lat> <lng> # Set geolocation
|
agent-browser set geo <lat> <lng> # Set geolocation
|
||||||
agent-browser set offline [on|off] # Toggle offline mode
|
agent-browser set offline [on|off] # Toggle offline mode
|
||||||
|
|||||||
+54
-5
@@ -1871,25 +1871,35 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
|||||||
Some("viewport") => {
|
Some("viewport") => {
|
||||||
let w_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
let w_str = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
context: "set viewport".to_string(),
|
context: "set viewport".to_string(),
|
||||||
usage: "set viewport <width> <height>",
|
usage: "set viewport <width> <height> [scale]",
|
||||||
})?;
|
})?;
|
||||||
let h_str = rest.get(2).ok_or_else(|| ParseError::MissingArguments {
|
let h_str = rest.get(2).ok_or_else(|| ParseError::MissingArguments {
|
||||||
context: "set viewport".to_string(),
|
context: "set viewport".to_string(),
|
||||||
usage: "set viewport <width> <height>",
|
usage: "set viewport <width> <height> [scale]",
|
||||||
})?;
|
})?;
|
||||||
let w = w_str
|
let w = w_str
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.map_err(|_| ParseError::MissingArguments {
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
context: "set viewport".to_string(),
|
context: "set viewport".to_string(),
|
||||||
usage: "set viewport <width> <height>",
|
usage: "set viewport <width> <height> [scale]",
|
||||||
})?;
|
})?;
|
||||||
let h = h_str
|
let h = h_str
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.map_err(|_| ParseError::MissingArguments {
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
context: "set viewport".to_string(),
|
context: "set viewport".to_string(),
|
||||||
usage: "set viewport <width> <height>",
|
usage: "set viewport <width> <height> [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::<f64>()
|
||||||
|
.map_err(|_| ParseError::MissingArguments {
|
||||||
|
context: "set viewport".to_string(),
|
||||||
|
usage: "set viewport <width> <height> [scale]",
|
||||||
|
})?;
|
||||||
|
cmd["deviceScaleFactor"] = json!(scale);
|
||||||
|
}
|
||||||
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
Some("device") => {
|
Some("device") => {
|
||||||
let dev = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
let dev = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
@@ -3045,6 +3055,45 @@ mod tests {
|
|||||||
assert_eq!(cmd["reducedMotion"], "reduce");
|
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]
|
#[test]
|
||||||
fn test_find_first_no_value() {
|
fn test_find_first_no_value() {
|
||||||
let cmd = parse_command(&args("find first a click"), &default_flags()).unwrap();
|
let cmd = parse_command(&args("find first a click"), &default_flags()).unwrap();
|
||||||
|
|||||||
@@ -777,6 +777,77 @@ async fn e2e_wait() {
|
|||||||
assert_success(&resp);
|
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
|
// Viewport and emulation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+2
-1
@@ -1590,7 +1590,7 @@ Usage: agent-browser set <setting> [args]
|
|||||||
Configures various browser settings and emulation options.
|
Configures various browser settings and emulation options.
|
||||||
|
|
||||||
Settings:
|
Settings:
|
||||||
viewport <w> <h> Set viewport size
|
viewport <w> <h> [scale] Set viewport size (scale = deviceScaleFactor, e.g. 2 for retina)
|
||||||
device <name> Emulate device (e.g., "iPhone 12")
|
device <name> Emulate device (e.g., "iPhone 12")
|
||||||
geo <lat> <lng> Set geolocation
|
geo <lat> <lng> Set geolocation
|
||||||
offline [on|off] Toggle offline mode
|
offline [on|off] Toggle offline mode
|
||||||
@@ -1605,6 +1605,7 @@ Global Options:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
agent-browser set viewport 1920 1080
|
agent-browser set viewport 1920 1080
|
||||||
|
agent-browser set viewport 1920 1080 2 # 2x retina
|
||||||
agent-browser set device "iPhone 12"
|
agent-browser set device "iPhone 12"
|
||||||
agent-browser set geo 37.7749 -122.4194
|
agent-browser set geo 37.7749 -122.4194
|
||||||
agent-browser set offline on
|
agent-browser set offline on
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ agent-browser mouse wheel <dy> [dx] # Scroll wheel
|
|||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-browser set viewport <w> <h> # Set viewport size
|
agent-browser set viewport <w> <h> [scale] # Set viewport size (scale for retina, e.g. 2)
|
||||||
agent-browser set device <name> # Emulate device ("iPhone 14")
|
agent-browser set device <name> # Emulate device ("iPhone 14")
|
||||||
agent-browser set geo <lat> <lng> # Set geolocation
|
agent-browser set geo <lat> <lng> # Set geolocation
|
||||||
agent-browser set offline [on|off] # Toggle offline mode
|
agent-browser set offline [on|off] # Toggle offline mode
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ agent-browser find nth 2 "a" hover
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-browser set viewport 1920 1080 # Set viewport size
|
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 device "iPhone 14" # Emulate device
|
||||||
agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation)
|
agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation)
|
||||||
agent-browser set offline on # Toggle offline mode
|
agent-browser set offline on # Toggle offline mode
|
||||||
|
|||||||
+25
-3
@@ -1465,11 +1465,33 @@ async function handleViewport(
|
|||||||
command: ViewportCommand,
|
command: ViewportCommand,
|
||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
await browser.setViewport(command.width, command.height);
|
if (command.deviceScaleFactor && command.deviceScaleFactor !== 1) {
|
||||||
return successResponse(command.id, {
|
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<string, unknown> = {
|
||||||
width: command.width,
|
width: command.width,
|
||||||
height: command.height,
|
height: command.height,
|
||||||
});
|
};
|
||||||
|
if (command.deviceScaleFactor !== undefined) {
|
||||||
|
result.deviceScaleFactor = command.deviceScaleFactor;
|
||||||
|
}
|
||||||
|
return successResponse(command.id, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUserAgent(
|
async function handleUserAgent(
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ const viewportSchema = baseCommandSchema.extend({
|
|||||||
action: z.literal('viewport'),
|
action: z.literal('viewport'),
|
||||||
width: z.number().positive(),
|
width: z.number().positive(),
|
||||||
height: z.number().positive(),
|
height: z.number().positive(),
|
||||||
|
deviceScaleFactor: z.number().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const userAgentSchema = baseCommandSchema.extend({
|
const userAgentSchema = baseCommandSchema.extend({
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ export interface ViewportCommand extends BaseCommand {
|
|||||||
action: 'viewport';
|
action: 'viewport';
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
deviceScaleFactor?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// User agent
|
// User agent
|
||||||
|
|||||||
Reference in New Issue
Block a user