mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
* feat(emulate): add mobile device emulation via CDP Emulation domain Add `bsk emulate` so agents can debug mobile page behaviour by emulating a device environment on a session tab: - Viewport metrics (Emulation.setDeviceMetricsOverride), User-Agent (Emulation.setUserAgentOverride, optional accept-language and UA client-hints metadata), and touch (Emulation.setTouchEmulationEnabled with maxTouchPoints). - Seven CLI-side device presets (iphone-14, iphone-14-pro-max, iphone-se, pixel-7, galaxy-s23, ipad-mini, galaxy-tab-s8) via --device; manual flags (--width/--height/--dpr/--mobile/--ua/ --accept-language/--touch/--max-touch-points) work standalone or override individual preset fields. - `bsk emulate --off` clears every override (clearDeviceMetricsOverride + touch disabled + empty UA override) and restores the real environment. - Overrides are per-tab (CDP per-target) and not inherited by new tabs; the result JSON carries a note reminding agents to re-apply. Wire: new tool.emulate method end-to-end (bsk-protocol types + JSON schemas + Method::ToolEmulate classified BrowserMutation, daemon forwarding, CLI subcommand, extension tool handler + ChromiumCdp Emulation wrappers, dispatcher registration), docs in both SKILL.md copies, and unit tests on every layer. * refine(emulate): merge per-tab state and align preset override semantics - extension: remember the applied emulation state per tab and merge each new request onto it field by field before applying, so a later --width/--height no longer resets an earlier preset's dpr/mobile; --off clears the stored state along with the CDP overrides - extension: cdp_failed errors now note that already-applied overrides were not rolled back and can be reset with --off - cli: validate width/height pairing and metric requirements on the merged preset+flags result, so --device iphone-14 --width 500 keeps the preset height - cli: add --no-mobile/--no-touch (conflicting with --mobile/--touch) to turn preset mobile/touch fields back off - docs: describe the merge behaviour, --no-mobile/--no-touch and --off state clearing in both SKILL.md copies
485 lines
12 KiB
Rust
485 lines
12 KiB
Rust
//! Smoke tests for clap-derive command parsing.
|
|
|
|
use std::time::Duration;
|
|
|
|
use bsk::cli::daemon::{DaemonCmd, parse_duration};
|
|
use bsk::cli::navigate::NavigateCmd;
|
|
use bsk::cli::record::{RecordCmd, RecordSub};
|
|
use bsk::{Cli, Command};
|
|
use clap::Parser;
|
|
|
|
fn parse(args: &[&str]) -> Cli {
|
|
Cli::try_parse_from(args).expect("clap parse should succeed")
|
|
}
|
|
|
|
#[test]
|
|
fn parses_daemon_start_with_defaults() {
|
|
let cli = parse(&["bsk", "daemon", "start"]);
|
|
let Command::Daemon(DaemonCmd::Start(args)) = cli.command else {
|
|
panic!("expected daemon start subcommand");
|
|
};
|
|
assert!(args.port.is_none());
|
|
assert!(!args.foreground);
|
|
assert_eq!(args.resolved_port(), 52800);
|
|
assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(600));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_daemon_start_with_flags() {
|
|
let cli = parse(&[
|
|
"bsk",
|
|
"daemon",
|
|
"start",
|
|
"--foreground",
|
|
"--port",
|
|
"52900",
|
|
"--daemon-idle",
|
|
"2s",
|
|
"--session-idle",
|
|
"30s",
|
|
]);
|
|
let Command::Daemon(DaemonCmd::Start(args)) = cli.command else {
|
|
panic!("expected daemon start subcommand");
|
|
};
|
|
assert!(args.foreground);
|
|
assert_eq!(args.resolved_port(), 52900);
|
|
assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(2));
|
|
assert_eq!(args.resolved_session_idle(), Duration::from_secs(30));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_daemon_stop_and_restart() {
|
|
let cli = parse(&["bsk", "daemon", "stop"]);
|
|
assert!(matches!(cli.command, Command::Daemon(DaemonCmd::Stop)));
|
|
|
|
let cli = parse(&["bsk", "daemon", "restart", "--foreground"]);
|
|
let Command::Daemon(DaemonCmd::Restart(args)) = cli.command else {
|
|
panic!("expected daemon restart subcommand");
|
|
};
|
|
assert!(args.foreground);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_top_level_status_and_doctor() {
|
|
let cli = parse(&["bsk", "status"]);
|
|
assert!(matches!(cli.command, Command::Status));
|
|
|
|
let cli = parse(&["bsk", "doctor"]);
|
|
assert!(matches!(cli.command, Command::Doctor));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_console_command_with_context_safety_flags() {
|
|
let cli = parse(&[
|
|
"bsk",
|
|
"console",
|
|
"--session",
|
|
"s1",
|
|
"--tab-id",
|
|
"9",
|
|
"--since",
|
|
"12",
|
|
"--limit",
|
|
"75",
|
|
"--max-text-chars",
|
|
"2048",
|
|
"--include-stack",
|
|
]);
|
|
let Command::Console(args) = cli.command else {
|
|
panic!("expected console command");
|
|
};
|
|
assert_eq!(args.session, "s1");
|
|
assert_eq!(args.tab_id, Some(9));
|
|
assert_eq!(args.since, Some(12));
|
|
assert_eq!(args.limit, Some(75));
|
|
assert_eq!(args.max_text_chars, Some(2048));
|
|
assert!(args.include_stack);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_zero_console_bounds() {
|
|
assert!(Cli::try_parse_from(["bsk", "console", "--session", "s1", "--limit", "0"]).is_err());
|
|
assert!(
|
|
Cli::try_parse_from(["bsk", "console", "--session", "s1", "--max-text-chars", "0"])
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_network_command_with_context_safety_flags() {
|
|
let cli = parse(&[
|
|
"bsk",
|
|
"network",
|
|
"--session",
|
|
"s1",
|
|
"--tab-id",
|
|
"9",
|
|
"--since",
|
|
"12",
|
|
"--limit",
|
|
"75",
|
|
"--max-text-chars",
|
|
"2048",
|
|
]);
|
|
let Command::Network(args) = cli.command else {
|
|
panic!("expected network command");
|
|
};
|
|
assert_eq!(args.session, "s1");
|
|
assert_eq!(args.tab_id, Some(9));
|
|
assert_eq!(args.since, Some(12));
|
|
assert_eq!(args.limit, Some(75));
|
|
assert_eq!(args.max_text_chars, Some(2048));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_zero_network_bounds() {
|
|
assert!(Cli::try_parse_from(["bsk", "network", "--session", "s1", "--limit", "0"]).is_err());
|
|
assert!(
|
|
Cli::try_parse_from(["bsk", "network", "--session", "s1", "--max-text-chars", "0"])
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_install_skill_subcommand() {
|
|
let cli = parse(&["bsk", "install-skill", "--list"]);
|
|
assert!(matches!(cli.command, Command::InstallSkill(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_update_subcommand_with_flags() {
|
|
let cli = parse(&["bsk", "update", "--check", "--yes", "--no-restart-daemon"]);
|
|
let Command::Update(args) = cli.command else {
|
|
panic!("expected update subcommand");
|
|
};
|
|
assert!(args.check);
|
|
assert!(args.yes);
|
|
assert!(!args.restart_daemon);
|
|
}
|
|
|
|
#[test]
|
|
fn duration_parser_accepts_units() {
|
|
assert_eq!(parse_duration("750ms").unwrap(), Duration::from_millis(750));
|
|
assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_nested_navigate_back_and_forward() {
|
|
let cli = parse(&["bsk", "navigate", "back", "--session", "s1"]);
|
|
let Command::Navigate(cmd) = cli.command else {
|
|
panic!("expected navigate command");
|
|
};
|
|
assert!(matches!(cmd.command, Some(NavigateCmd::Back(_))));
|
|
|
|
let cli = parse(&["bsk", "navigate", "forward", "--session", "s1"]);
|
|
let Command::Navigate(cmd) = cli.command else {
|
|
panic!("expected navigate command");
|
|
};
|
|
assert!(matches!(cmd.command, Some(NavigateCmd::Forward(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_click_count_alias() {
|
|
let cli = parse(&["bsk", "click", "@e1", "--session", "s1", "--count", "2"]);
|
|
let Command::Click(args) = cli.command else {
|
|
panic!("expected click command");
|
|
};
|
|
assert_eq!(args.click_count, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_zero_click_count() {
|
|
assert!(
|
|
Cli::try_parse_from(["bsk", "click", "@e1", "--session", "s1", "--count", "0"]).is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_record_start_with_browser_and_url() {
|
|
let cli = parse(&[
|
|
"bsk",
|
|
"record",
|
|
"start",
|
|
"--browser",
|
|
"022ca8ac",
|
|
"--url",
|
|
"https://x",
|
|
]);
|
|
let Command::Record(RecordCmd {
|
|
sub: RecordSub::Start(args),
|
|
}) = cli.command
|
|
else {
|
|
panic!("expected record start subcommand");
|
|
};
|
|
assert_eq!(args.browser.as_deref(), Some("022ca8ac"));
|
|
assert_eq!(args.url.as_deref(), Some("https://x"));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_record_start_without_url() {
|
|
let cli = parse(&["bsk", "record", "start", "--browser", "022ca8ac"]);
|
|
let Command::Record(RecordCmd {
|
|
sub: RecordSub::Start(args),
|
|
}) = cli.command
|
|
else {
|
|
panic!("expected record start subcommand");
|
|
};
|
|
assert_eq!(args.browser.as_deref(), Some("022ca8ac"));
|
|
assert!(args.url.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_session_start_with_window_size() {
|
|
use bsk::cli::session::{SessionCmd, SessionSub};
|
|
let cli = parse(&[
|
|
"bsk", "session", "start", "--width", "1280", "--height", "800",
|
|
]);
|
|
let Command::Session(SessionCmd {
|
|
sub: SessionSub::Start(args),
|
|
}) = cli.command
|
|
else {
|
|
panic!("expected session start subcommand");
|
|
};
|
|
assert_eq!(args.width, Some(1280));
|
|
assert_eq!(args.height, Some(800));
|
|
}
|
|
|
|
#[test]
|
|
fn session_start_window_size_defaults_to_none() {
|
|
use bsk::cli::session::{SessionCmd, SessionSub};
|
|
let cli = parse(&["bsk", "session", "start"]);
|
|
let Command::Session(SessionCmd {
|
|
sub: SessionSub::Start(args),
|
|
}) = cli.command
|
|
else {
|
|
panic!("expected session start subcommand");
|
|
};
|
|
assert!(args.width.is_none());
|
|
assert!(args.height.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_out_of_range_session_start_window_size() {
|
|
assert!(Cli::try_parse_from(["bsk", "session", "start", "--width", "99"]).is_err());
|
|
assert!(Cli::try_parse_from(["bsk", "session", "start", "--height", "7681"]).is_err());
|
|
assert!(Cli::try_parse_from(["bsk", "session", "start", "--width", "abc"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_window_resize() {
|
|
use bsk::cli::window::{WindowCmd, WindowSub};
|
|
let cli = parse(&[
|
|
"bsk",
|
|
"window",
|
|
"resize",
|
|
"--session",
|
|
"s1",
|
|
"--width",
|
|
"1280",
|
|
"--height",
|
|
"800",
|
|
]);
|
|
let Command::Window(WindowCmd {
|
|
sub: WindowSub::Resize(args),
|
|
}) = cli.command
|
|
else {
|
|
panic!("expected window resize subcommand");
|
|
};
|
|
assert_eq!(args.session, "s1");
|
|
assert_eq!(args.width, 1280);
|
|
assert_eq!(args.height, 800);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_window_resize_dimensions() {
|
|
assert!(
|
|
Cli::try_parse_from([
|
|
"bsk",
|
|
"window",
|
|
"resize",
|
|
"--session",
|
|
"s1",
|
|
"--width",
|
|
"99",
|
|
"--height",
|
|
"800"
|
|
])
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
Cli::try_parse_from([
|
|
"bsk",
|
|
"window",
|
|
"resize",
|
|
"--session",
|
|
"s1",
|
|
"--width",
|
|
"1280",
|
|
"--height",
|
|
"7681"
|
|
])
|
|
.is_err()
|
|
);
|
|
// width/height are required for resize.
|
|
assert!(Cli::try_parse_from(["bsk", "window", "resize", "--session", "s1"]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_emulate_with_device_preset() {
|
|
use bsk::cli::emulate::EmulateArgs;
|
|
let cli = parse(&["bsk", "emulate", "--session", "s1", "--device", "iphone-14"]);
|
|
let Command::Emulate(EmulateArgs {
|
|
session,
|
|
device,
|
|
off,
|
|
..
|
|
}) = cli.command
|
|
else {
|
|
panic!("expected emulate subcommand");
|
|
};
|
|
assert_eq!(session, "s1");
|
|
assert_eq!(device.as_deref(), Some("iphone-14"));
|
|
assert!(!off);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_emulate_manual_overrides() {
|
|
let cli = parse(&[
|
|
"bsk",
|
|
"emulate",
|
|
"--session",
|
|
"s1",
|
|
"--width",
|
|
"390",
|
|
"--height",
|
|
"844",
|
|
"--dpr",
|
|
"3",
|
|
"--mobile",
|
|
"--ua",
|
|
"Mozilla/5.0 (iPhone)",
|
|
"--accept-language",
|
|
"zh-CN",
|
|
"--touch",
|
|
"--max-touch-points",
|
|
"5",
|
|
"--tab-id",
|
|
"7",
|
|
]);
|
|
let Command::Emulate(args) = cli.command else {
|
|
panic!("expected emulate subcommand");
|
|
};
|
|
assert_eq!(args.width, Some(390));
|
|
assert_eq!(args.height, Some(844));
|
|
assert_eq!(args.dpr, Some(3.0));
|
|
assert!(args.mobile);
|
|
assert_eq!(args.ua.as_deref(), Some("Mozilla/5.0 (iPhone)"));
|
|
assert_eq!(args.accept_language.as_deref(), Some("zh-CN"));
|
|
assert!(args.touch);
|
|
assert_eq!(args.max_touch_points, Some(5));
|
|
assert_eq!(args.tab_id, Some(7));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_emulate_off() {
|
|
let cli = parse(&["bsk", "emulate", "--session", "s1", "--off"]);
|
|
let Command::Emulate(args) = cli.command else {
|
|
panic!("expected emulate subcommand");
|
|
};
|
|
assert!(args.off);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_emulate_no_mobile_no_touch() {
|
|
let cli = parse(&[
|
|
"bsk",
|
|
"emulate",
|
|
"--session",
|
|
"s1",
|
|
"--device",
|
|
"iphone-14",
|
|
"--no-mobile",
|
|
"--no-touch",
|
|
]);
|
|
let Command::Emulate(args) = cli.command else {
|
|
panic!("expected emulate subcommand");
|
|
};
|
|
assert!(args.no_mobile);
|
|
assert!(args.no_touch);
|
|
assert!(!args.mobile);
|
|
assert!(!args.touch);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_conflicting_emulate_flags() {
|
|
for extra in [
|
|
&["--mobile", "--no-mobile"][..],
|
|
&["--touch", "--no-touch"][..],
|
|
] {
|
|
let mut argv = vec!["bsk", "emulate", "--session", "s1", "--device", "iphone-14"];
|
|
argv.extend_from_slice(extra);
|
|
assert!(Cli::try_parse_from(argv).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_emulate_values() {
|
|
// Zero / out-of-range dimensions.
|
|
assert!(
|
|
Cli::try_parse_from([
|
|
"bsk",
|
|
"emulate",
|
|
"--session",
|
|
"s1",
|
|
"--width",
|
|
"0",
|
|
"--height",
|
|
"844"
|
|
])
|
|
.is_err()
|
|
);
|
|
// Non-positive dpr.
|
|
assert!(
|
|
Cli::try_parse_from([
|
|
"bsk",
|
|
"emulate",
|
|
"--session",
|
|
"s1",
|
|
"--width",
|
|
"390",
|
|
"--height",
|
|
"844",
|
|
"--dpr",
|
|
"0"
|
|
])
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
Cli::try_parse_from([
|
|
"bsk",
|
|
"emulate",
|
|
"--session",
|
|
"s1",
|
|
"--width",
|
|
"390",
|
|
"--height",
|
|
"844",
|
|
"--dpr",
|
|
"abc"
|
|
])
|
|
.is_err()
|
|
);
|
|
// Zero touch points.
|
|
assert!(
|
|
Cli::try_parse_from([
|
|
"bsk",
|
|
"emulate",
|
|
"--session",
|
|
"s1",
|
|
"--max-touch-points",
|
|
"0"
|
|
])
|
|
.is_err()
|
|
);
|
|
}
|