mirror of
https://github.com/actionbook/actionbook.git
synced 2026-09-19 03:28:06 +08:00
1611 lines
56 KiB
Rust
1611 lines
56 KiB
Rust
use clap::{Args, Parser, Subcommand};
|
|
|
|
use crate::action::Action;
|
|
use crate::action_result::ActionResult;
|
|
use crate::browser::{cookies, interaction, navigation, observation, session, storage, tab, wait};
|
|
use crate::output::ResponseContext;
|
|
use crate::setup;
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(
|
|
name = "actionbook",
|
|
about = "Actionbook CLI - Browser automation for AI agents",
|
|
disable_version_flag = true
|
|
)]
|
|
pub struct Cli {
|
|
/// JSON output (default is plain text)
|
|
#[arg(long, global = true)]
|
|
pub json: bool,
|
|
|
|
/// Timeout in milliseconds
|
|
#[arg(long, global = true)]
|
|
pub timeout: Option<u64>,
|
|
|
|
/// API key for authenticated access
|
|
#[arg(
|
|
long,
|
|
env = "ACTIONBOOK_API_KEY",
|
|
global = true,
|
|
hide_env_values = true
|
|
)]
|
|
pub api_key: Option<String>,
|
|
|
|
/// Print version
|
|
#[arg(long, short = 'v')]
|
|
pub version: bool,
|
|
|
|
#[command(subcommand)]
|
|
pub command: Option<Commands>,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum Commands {
|
|
/// Search for action manuals by keyword
|
|
Search {
|
|
/// Search keyword
|
|
keyword: String,
|
|
},
|
|
|
|
/// Get detailed manual information for a site, group, or action
|
|
#[command(alias = "man")]
|
|
Manual {
|
|
/// Site name
|
|
site: Option<String>,
|
|
/// Group name
|
|
group: Option<String>,
|
|
/// Action name
|
|
action: Option<String>,
|
|
},
|
|
|
|
/// Get complete action details by area ID
|
|
Get {
|
|
/// Area ID (e.g., "airbnb.com:/:default")
|
|
area_id: String,
|
|
},
|
|
|
|
/// Browser automation commands
|
|
Browser {
|
|
#[command(subcommand)]
|
|
command: BrowserCommands,
|
|
},
|
|
/// Daemon lifecycle management
|
|
Daemon {
|
|
#[command(subcommand)]
|
|
command: DaemonCommands,
|
|
},
|
|
/// Manage the Actionbook browser extension
|
|
Extension {
|
|
#[command(subcommand)]
|
|
command: ExtensionCommands,
|
|
},
|
|
/// Interactive configuration wizard
|
|
Setup(setup::Cmd),
|
|
/// Show help
|
|
Help,
|
|
/// Print version
|
|
Version,
|
|
}
|
|
|
|
/// Daemon-level subcommands. The daemon itself runs via the hidden `__daemon`
|
|
/// flag — these commands are user-facing controls over its lifecycle.
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum DaemonCommands {
|
|
/// Stop the running daemon. The next CLI call will auto-spawn a fresh one.
|
|
///
|
|
/// Use this to recover from a stuck bridge (e.g. `BRIDGE_BIND_FAILED`
|
|
/// after the holding process has been freed) without manually finding
|
|
/// the daemon pid.
|
|
Restart,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum ExtensionCommands {
|
|
/// Show extension status (bridge + connection)
|
|
Status,
|
|
/// Ping the extension bridge and measure RTT
|
|
Ping,
|
|
/// Show extension install path and installed status
|
|
Path,
|
|
/// Install the Actionbook extension
|
|
Install(ExtensionInstallArgs),
|
|
/// Uninstall the Actionbook extension
|
|
Uninstall,
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct ExtensionInstallArgs {
|
|
/// Force overwrite of an existing installation
|
|
#[arg(long)]
|
|
pub force: bool,
|
|
}
|
|
|
|
/// Unimplemented tab-level command args.
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct TabArgs {
|
|
/// Session ID
|
|
#[arg(long)]
|
|
pub session: String,
|
|
/// Tab ID
|
|
#[arg(long)]
|
|
pub tab: String,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum BrowserCommands {
|
|
/// Show browser help
|
|
Help,
|
|
/// Diagnose browser daemon/hand health without requiring an existing session
|
|
Doctor(BrowserDoctorArgs),
|
|
|
|
// ── Session lifecycle ──────────────────────────────────────
|
|
/// Start or attach a browser session
|
|
Start(session::start::Cmd),
|
|
/// List all active sessions
|
|
ListSessions(session::list::Cmd),
|
|
/// Show session status
|
|
Status(session::status::Cmd),
|
|
/// Close a session
|
|
#[command(alias = "stop")]
|
|
Close(session::close::Cmd),
|
|
/// Restart a session
|
|
Restart(session::restart::Cmd),
|
|
|
|
// ── Tab management ─────────────────────────────────────────
|
|
/// List tabs in a session
|
|
ListTabs(tab::list::Cmd),
|
|
/// Open a new tab
|
|
#[command(alias = "open")]
|
|
NewTab(tab::open::Cmd),
|
|
/// Open multiple tabs in one call (batch)
|
|
#[command(alias = "batch-open")]
|
|
BatchNewTab(tab::batch_open::Cmd),
|
|
/// Close a tab
|
|
CloseTab(tab::close::Cmd),
|
|
|
|
// ── Navigation ─────────────────────────────────────────────
|
|
/// Navigate to URL
|
|
Goto(navigation::goto::Cmd),
|
|
/// Go back
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser back --session s1 --tab t1")]
|
|
Back(TabArgs),
|
|
/// Go forward
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser forward --session s1 --tab t1")]
|
|
Forward(TabArgs),
|
|
/// Reload page
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser reload --session s1 --tab t1")]
|
|
Reload(TabArgs),
|
|
|
|
// ── Observation ────────────────────────────────────────────
|
|
/// Capture accessibility snapshots for multiple tabs
|
|
BatchSnapshot(observation::batch_snapshot::Cmd),
|
|
/// Capture accessibility snapshot
|
|
Snapshot(observation::snapshot::Cmd),
|
|
/// Get current page title
|
|
Title(observation::title::Cmd),
|
|
/// Get current page URL
|
|
Url(observation::url::Cmd),
|
|
/// Get viewport dimensions
|
|
Viewport(observation::viewport::Cmd),
|
|
/// Read element or page HTML
|
|
Html(observation::html::Cmd),
|
|
/// Read element or page text
|
|
Text(observation::text::Cmd),
|
|
/// Read element value
|
|
Value(observation::value::Cmd),
|
|
/// Read a named element attribute
|
|
Attr(observation::attr::Cmd),
|
|
/// Read all attributes on an element
|
|
Attrs(observation::attrs::Cmd),
|
|
/// Read an element bounding box
|
|
Box(observation::r#box::Cmd),
|
|
/// Read computed styles for an element
|
|
Styles(observation::styles::Cmd),
|
|
/// Describe element properties and context
|
|
Describe(observation::describe::Cmd),
|
|
/// Get element state
|
|
State(observation::state::Cmd),
|
|
/// Query elements with cardinality constraints
|
|
Query(observation::query::Cmd),
|
|
/// Inspect element at coordinates
|
|
InspectPoint(observation::inspect_point::Cmd),
|
|
/// Save page as PDF
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser pdf /tmp/page.pdf --session s1 --tab t1")]
|
|
Pdf(observation::pdf::Cmd),
|
|
/// Get browser console or error logs
|
|
Logs {
|
|
#[command(subcommand)]
|
|
command: LogsCommands,
|
|
},
|
|
/// Observe network requests
|
|
Network {
|
|
#[command(subcommand)]
|
|
command: NetworkCommands,
|
|
},
|
|
/// Take screenshot
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser screenshot /tmp/page.png --session s1 --tab t1")]
|
|
Screenshot(observation::screenshot::Cmd),
|
|
|
|
// ── Cookies ────────────────────────────────────────────────
|
|
/// Manage browser cookies
|
|
Cookies {
|
|
#[command(subcommand)]
|
|
command: CookiesCommands,
|
|
},
|
|
|
|
// ── Storage ────────────────────────────────────────────────
|
|
/// Manage local storage (window.localStorage)
|
|
#[command(name = "local-storage")]
|
|
LocalStorage {
|
|
#[command(subcommand)]
|
|
command: StorageSubCommands,
|
|
},
|
|
/// Manage session storage (window.sessionStorage)
|
|
#[command(name = "session-storage")]
|
|
SessionStorage {
|
|
#[command(subcommand)]
|
|
command: StorageSubCommands,
|
|
},
|
|
|
|
// ── Wait ───────────────────────────────────────────────────
|
|
/// Wait for a browser condition
|
|
Wait {
|
|
#[command(subcommand)]
|
|
command: WaitCommands,
|
|
},
|
|
|
|
// ── Interaction ────────────────────────────────────────────
|
|
/// Evaluate JavaScript
|
|
Eval(interaction::eval::Cmd),
|
|
/// Click an element
|
|
Click(interaction::click::Cmd),
|
|
/// Click multiple elements in sequence (batch)
|
|
BatchClick(interaction::batch_click::Cmd),
|
|
/// Hover over an element
|
|
Hover(interaction::hover::Cmd),
|
|
/// Focus an element
|
|
Focus(interaction::focus::Cmd),
|
|
/// Press a key or key combination
|
|
Press(interaction::press::Cmd),
|
|
/// Fill an input field
|
|
Fill(interaction::fill::Cmd),
|
|
/// Type text (keystroke by keystroke)
|
|
Type(interaction::type_text::Cmd),
|
|
/// Select a value from a dropdown
|
|
Select(interaction::select::Cmd),
|
|
/// Drag an element to a target
|
|
Drag(interaction::drag::Cmd),
|
|
/// Upload files to a file input
|
|
Upload(interaction::upload::Cmd),
|
|
/// Move the mouse to absolute coordinates
|
|
MouseMove(interaction::mouse_move::Cmd),
|
|
/// Get the current cursor position
|
|
CursorPosition(interaction::cursor_position::Cmd),
|
|
/// Scroll the page or a container
|
|
Scroll(interaction::scroll::Cmd),
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum WaitCommands {
|
|
/// Wait for a CSS selector to appear in the DOM
|
|
Element(wait::element::Cmd),
|
|
/// Wait for a navigation to complete
|
|
Navigation(wait::navigation::Cmd),
|
|
/// Wait for network activity to become idle
|
|
#[command(name = "network-idle")]
|
|
NetworkIdle(wait::network_idle::Cmd),
|
|
/// Wait for a JavaScript expression to become truthy
|
|
Condition(wait::condition::Cmd),
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum LogsCommands {
|
|
/// Get console logs (console.log/info/warn/error/debug)
|
|
Console(observation::logs_console::Cmd),
|
|
/// Get error logs (window error events + unhandled rejections)
|
|
Errors(observation::logs_errors::Cmd),
|
|
/// Read daemon/browser process logs
|
|
Daemon(DaemonLogsArgs),
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct BrowserDoctorArgs {
|
|
/// Start the daemon if it is not running, then re-check health.
|
|
#[arg(long)]
|
|
pub start: bool,
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct DaemonLogsArgs {
|
|
/// Return only the last N log lines.
|
|
#[arg(long, default_value_t = 100)]
|
|
pub tail: usize,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum NetworkCommands {
|
|
/// List tracked network requests for a tab
|
|
Requests(observation::network_requests::Cmd),
|
|
/// Get detail for a single network request (including response body)
|
|
Request(observation::network_request_detail::Cmd),
|
|
/// HAR recording (start / stop)
|
|
Har {
|
|
#[command(subcommand)]
|
|
command: HarCommands,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum HarCommands {
|
|
/// Start HAR recording for a tab
|
|
Start(observation::network_har::StartCmd),
|
|
/// Stop HAR recording and export to a file
|
|
Stop(observation::network_har::StopCmd),
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum CookiesCommands {
|
|
/// List all cookies (optionally filtered by domain)
|
|
List(cookies::list::Cmd),
|
|
/// Get a single cookie by name
|
|
Get(cookies::get::Cmd),
|
|
/// Set a cookie
|
|
Set(cookies::set::Cmd),
|
|
/// Delete a cookie by name
|
|
Delete(cookies::delete::Cmd),
|
|
/// Clear cookies (optionally filtered by domain)
|
|
Clear(cookies::clear::Cmd),
|
|
}
|
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
#[command(disable_help_subcommand = true)]
|
|
pub enum StorageSubCommands {
|
|
/// List all key-value entries
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser local-storage list --session s1 --tab t1
|
|
actionbook browser session-storage list --session s1 --tab t1
|
|
|
|
Returns all key-value pairs in the storage object.")]
|
|
List(StorageArgs),
|
|
/// Get a value by key
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser local-storage get auth_token --session s1 --tab t1
|
|
actionbook browser session-storage get user_id --session s1 --tab t1
|
|
|
|
Returns null if the key does not exist.")]
|
|
Get(StorageKeyArgs),
|
|
/// Set a key-value entry
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser local-storage set theme dark --session s1 --tab t1
|
|
actionbook browser session-storage set lang en --session s1 --tab t1
|
|
|
|
Creates the key if it does not exist, overwrites if it does.")]
|
|
Set(StorageSetArgs),
|
|
/// Delete a key
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser local-storage delete auth_token --session s1 --tab t1
|
|
actionbook browser session-storage delete temp_data --session s1 --tab t1
|
|
|
|
Removes the key entirely. No-op if the key does not exist.")]
|
|
Delete(StorageKeyArgs),
|
|
/// Clear the value for a key
|
|
#[command(after_help = "\
|
|
Examples:
|
|
actionbook browser local-storage clear cache_key --session s1 --tab t1
|
|
actionbook browser session-storage clear pref --session s1 --tab t1
|
|
|
|
Removes the key from storage. Returns affected count (1 if existed, 0 if not).")]
|
|
Clear(StorageKeyArgs),
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct StorageArgs {
|
|
#[arg(long)]
|
|
pub session: String,
|
|
#[arg(long)]
|
|
pub tab: String,
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct StorageKeyArgs {
|
|
pub key: String,
|
|
#[arg(long)]
|
|
pub session: String,
|
|
#[arg(long)]
|
|
pub tab: String,
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone)]
|
|
pub struct StorageSetArgs {
|
|
pub key: String,
|
|
pub value: String,
|
|
#[arg(long)]
|
|
pub session: String,
|
|
#[arg(long)]
|
|
pub tab: String,
|
|
}
|
|
|
|
impl BrowserCommands {
|
|
/// Convert to wire Action. Returns None for unimplemented commands.
|
|
pub fn to_action(&self) -> Option<Action> {
|
|
Some(match self {
|
|
Self::Help => return None,
|
|
Self::Doctor(_) => return None,
|
|
Self::Start(cmd) => Action::StartSession(cmd.clone()),
|
|
Self::ListSessions(cmd) => Action::ListSessions(cmd.clone()),
|
|
Self::Status(cmd) => Action::SessionStatus(cmd.clone()),
|
|
Self::Close(cmd) => Action::Close(cmd.clone()),
|
|
Self::Restart(cmd) => Action::Restart(cmd.clone()),
|
|
Self::ListTabs(cmd) => Action::ListTabs(cmd.clone()),
|
|
Self::NewTab(cmd) => Action::NewTab(cmd.clone()),
|
|
Self::BatchNewTab(cmd) => Action::BatchOpen(cmd.clone()),
|
|
Self::CloseTab(cmd) => Action::CloseTab(cmd.clone()),
|
|
Self::Goto(cmd) => Action::Goto(cmd.clone()),
|
|
Self::Back(a) => Action::Back(navigation::back::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
}),
|
|
Self::Forward(a) => Action::Forward(navigation::forward::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
}),
|
|
Self::Reload(a) => Action::Reload(navigation::reload::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
}),
|
|
Self::BatchSnapshot(cmd) => Action::BatchSnapshot(cmd.clone()),
|
|
Self::Snapshot(cmd) => Action::Snapshot(cmd.clone()),
|
|
Self::Title(cmd) => Action::Title(cmd.clone()),
|
|
Self::Url(cmd) => Action::Url(cmd.clone()),
|
|
Self::Viewport(cmd) => Action::Viewport(cmd.clone()),
|
|
Self::Html(cmd) => Action::Html(cmd.clone()),
|
|
Self::Text(cmd) => Action::Text(cmd.clone()),
|
|
Self::Value(cmd) => Action::Value(cmd.clone()),
|
|
Self::Attr(cmd) => Action::Attr(cmd.clone()),
|
|
Self::Attrs(cmd) => Action::Attrs(cmd.clone()),
|
|
Self::Box(cmd) => Action::Box(cmd.clone()),
|
|
Self::Styles(cmd) => Action::Styles(cmd.clone()),
|
|
Self::Describe(cmd) => Action::Describe(cmd.clone()),
|
|
Self::State(cmd) => Action::State(cmd.clone()),
|
|
Self::Query(cmd) => Action::Query(cmd.clone()),
|
|
Self::InspectPoint(cmd) => Action::InspectPoint(cmd.clone()),
|
|
Self::Pdf(cmd) => Action::Pdf(cmd.clone()),
|
|
Self::Cookies { command } => match command {
|
|
CookiesCommands::List(cmd) => Action::CookiesList(cmd.clone()),
|
|
CookiesCommands::Get(cmd) => Action::CookiesGet(cmd.clone()),
|
|
CookiesCommands::Set(cmd) => Action::CookiesSet(cmd.clone()),
|
|
CookiesCommands::Delete(cmd) => Action::CookiesDelete(cmd.clone()),
|
|
CookiesCommands::Clear(cmd) => Action::CookiesClear(cmd.clone()),
|
|
},
|
|
Self::LocalStorage { command } => {
|
|
storage_to_action(command, storage::StorageKind::Local)
|
|
}
|
|
Self::SessionStorage { command } => {
|
|
storage_to_action(command, storage::StorageKind::Session)
|
|
}
|
|
Self::Logs { command } => match command {
|
|
LogsCommands::Console(cmd) => Action::LogsConsole(cmd.clone()),
|
|
LogsCommands::Errors(cmd) => Action::LogsErrors(cmd.clone()),
|
|
LogsCommands::Daemon(_) => return None,
|
|
},
|
|
Self::Network { command } => match command {
|
|
NetworkCommands::Requests(cmd) => Action::NetworkRequests(cmd.clone()),
|
|
NetworkCommands::Request(cmd) => Action::NetworkRequestDetail(cmd.clone()),
|
|
NetworkCommands::Har { command } => match command {
|
|
HarCommands::Start(cmd) => Action::NetworkHarStart(cmd.clone()),
|
|
HarCommands::Stop(cmd) => {
|
|
// Resolve --out against the CLI's CWD before handing
|
|
// the command to the daemon. The daemon may have been
|
|
// spawned from a different directory, so relying on
|
|
// the daemon's CWD would produce a surprising location
|
|
// for a user typing `--out test.har` from their shell.
|
|
let mut cmd = cmd.clone();
|
|
if let Some(ref p) = cmd.out {
|
|
let path = std::path::Path::new(p);
|
|
if let Ok(abs) = std::path::absolute(path) {
|
|
cmd.out = Some(abs.to_string_lossy().into_owned());
|
|
}
|
|
}
|
|
Action::NetworkHarStop(cmd)
|
|
}
|
|
},
|
|
},
|
|
Self::Wait { command } => match command {
|
|
WaitCommands::Element(cmd) => Action::WaitElement(cmd.clone()),
|
|
WaitCommands::Navigation(cmd) => Action::WaitNavigation(cmd.clone()),
|
|
WaitCommands::NetworkIdle(cmd) => Action::WaitNetworkIdle(cmd.clone()),
|
|
WaitCommands::Condition(cmd) => Action::WaitCondition(cmd.clone()),
|
|
},
|
|
Self::Screenshot(cmd) => Action::Screenshot(cmd.clone()),
|
|
Self::Eval(cmd) => Action::Eval(cmd.clone()),
|
|
Self::Click(cmd) => Action::Click(cmd.clone()),
|
|
Self::BatchClick(cmd) => Action::BatchClick(cmd.clone()),
|
|
Self::Hover(cmd) => Action::Hover(cmd.clone()),
|
|
Self::Focus(cmd) => Action::Focus(cmd.clone()),
|
|
Self::Press(cmd) => Action::Press(cmd.clone()),
|
|
Self::Type(cmd) => Action::Type(cmd.clone()),
|
|
Self::Fill(cmd) => Action::Fill(cmd.clone()),
|
|
Self::Select(cmd) => Action::Select(cmd.clone()),
|
|
Self::Drag(cmd) => Action::Drag(cmd.clone()),
|
|
Self::Upload(cmd) => Action::Upload(cmd.clone()),
|
|
Self::MouseMove(cmd) => Action::MouseMove(cmd.clone()),
|
|
Self::CursorPosition(cmd) => Action::CursorPosition(cmd.clone()),
|
|
Self::Scroll(cmd) => Action::Scroll(cmd.clone()),
|
|
})
|
|
}
|
|
|
|
/// Normalized command name for the JSON envelope.
|
|
pub fn command_name(&self) -> &str {
|
|
match self {
|
|
Self::Help => "help",
|
|
Self::Doctor(_) => "browser doctor",
|
|
Self::Start(_) => session::start::COMMAND_NAME,
|
|
Self::ListSessions(_) => session::list::COMMAND_NAME,
|
|
Self::Status(_) => session::status::COMMAND_NAME,
|
|
Self::Close(_) => session::close::COMMAND_NAME,
|
|
Self::Restart(_) => session::restart::COMMAND_NAME,
|
|
Self::ListTabs(_) => tab::list::COMMAND_NAME,
|
|
Self::NewTab(_) => tab::open::COMMAND_NAME,
|
|
Self::BatchNewTab(_) => tab::batch_open::COMMAND_NAME,
|
|
Self::CloseTab(_) => tab::close::COMMAND_NAME,
|
|
Self::Goto(_) => navigation::goto::COMMAND_NAME,
|
|
Self::Back(_) => "browser back",
|
|
Self::Forward(_) => "browser forward",
|
|
Self::Reload(_) => "browser reload",
|
|
Self::BatchSnapshot(_) => observation::batch_snapshot::COMMAND_NAME,
|
|
Self::Snapshot(_) => observation::snapshot::COMMAND_NAME,
|
|
Self::Title(_) => observation::title::COMMAND_NAME,
|
|
Self::Url(_) => observation::url::COMMAND_NAME,
|
|
Self::Viewport(_) => observation::viewport::COMMAND_NAME,
|
|
Self::Html(_) => observation::html::COMMAND_NAME,
|
|
Self::Text(_) => observation::text::COMMAND_NAME,
|
|
Self::Value(_) => observation::value::COMMAND_NAME,
|
|
Self::Attr(_) => observation::attr::COMMAND_NAME,
|
|
Self::Attrs(_) => observation::attrs::COMMAND_NAME,
|
|
Self::Box(_) => observation::r#box::COMMAND_NAME,
|
|
Self::Styles(_) => observation::styles::COMMAND_NAME,
|
|
Self::Describe(_) => observation::describe::COMMAND_NAME,
|
|
Self::State(_) => observation::state::COMMAND_NAME,
|
|
Self::Query(_) => observation::query::COMMAND_NAME,
|
|
Self::InspectPoint(_) => observation::inspect_point::COMMAND_NAME,
|
|
Self::Pdf(_) => observation::pdf::COMMAND_NAME,
|
|
Self::Cookies { command } => match command {
|
|
CookiesCommands::List(_) => cookies::list::COMMAND_NAME,
|
|
CookiesCommands::Get(_) => cookies::get::COMMAND_NAME,
|
|
CookiesCommands::Set(_) => cookies::set::COMMAND_NAME,
|
|
CookiesCommands::Delete(_) => cookies::delete::COMMAND_NAME,
|
|
CookiesCommands::Clear(_) => cookies::clear::COMMAND_NAME,
|
|
},
|
|
Self::LocalStorage { command } => {
|
|
storage_command_name(command, storage::StorageKind::Local)
|
|
}
|
|
Self::SessionStorage { command } => {
|
|
storage_command_name(command, storage::StorageKind::Session)
|
|
}
|
|
Self::Logs { command } => match command {
|
|
LogsCommands::Console(_) => observation::logs_console::COMMAND_NAME,
|
|
LogsCommands::Errors(_) => observation::logs_errors::COMMAND_NAME,
|
|
LogsCommands::Daemon(_) => "browser logs daemon",
|
|
},
|
|
Self::Network { command } => match command {
|
|
NetworkCommands::Requests(_) => observation::network_requests::COMMAND_NAME,
|
|
NetworkCommands::Request(_) => observation::network_request_detail::COMMAND_NAME,
|
|
NetworkCommands::Har { command } => match command {
|
|
HarCommands::Start(_) => observation::network_har::START_COMMAND_NAME,
|
|
HarCommands::Stop(_) => observation::network_har::STOP_COMMAND_NAME,
|
|
},
|
|
},
|
|
Self::Wait { command } => match command {
|
|
WaitCommands::Element(_) => wait::element::COMMAND_NAME,
|
|
WaitCommands::Navigation(_) => wait::navigation::COMMAND_NAME,
|
|
WaitCommands::NetworkIdle(_) => wait::network_idle::COMMAND_NAME,
|
|
WaitCommands::Condition(_) => wait::condition::COMMAND_NAME,
|
|
},
|
|
Self::Screenshot(_) => observation::screenshot::COMMAND_NAME,
|
|
Self::Eval(_) => interaction::eval::COMMAND_NAME,
|
|
Self::Click(_) => interaction::click::COMMAND_NAME,
|
|
Self::BatchClick(_) => interaction::batch_click::COMMAND_NAME,
|
|
Self::Hover(_) => interaction::hover::COMMAND_NAME,
|
|
Self::Focus(_) => interaction::focus::COMMAND_NAME,
|
|
Self::Press(_) => interaction::press::COMMAND_NAME,
|
|
Self::Fill(_) => interaction::fill::COMMAND_NAME,
|
|
Self::Type(_) => interaction::type_text::COMMAND_NAME,
|
|
Self::Select(_) => interaction::select::COMMAND_NAME,
|
|
Self::Drag(_) => interaction::drag::COMMAND_NAME,
|
|
Self::Upload(_) => interaction::upload::COMMAND_NAME,
|
|
Self::MouseMove(_) => interaction::mouse_move::COMMAND_NAME,
|
|
Self::CursorPosition(_) => interaction::cursor_position::COMMAND_NAME,
|
|
Self::Scroll(_) => interaction::scroll::COMMAND_NAME,
|
|
}
|
|
}
|
|
|
|
/// Build response context from command args and result.
|
|
pub fn context(&self, result: &ActionResult) -> Option<ResponseContext> {
|
|
match self {
|
|
Self::Help => None,
|
|
Self::Doctor(_) => None,
|
|
Self::Start(cmd) => session::start::context(cmd, result),
|
|
Self::ListSessions(cmd) => session::list::context(cmd, result),
|
|
Self::Status(cmd) => session::status::context(cmd, result),
|
|
Self::Close(cmd) => session::close::context(cmd, result),
|
|
Self::Restart(cmd) => session::restart::context(cmd, result),
|
|
Self::ListTabs(cmd) => tab::list::context(cmd, result),
|
|
Self::NewTab(cmd) => tab::open::context(cmd, result),
|
|
Self::BatchNewTab(cmd) => tab::batch_open::context(cmd, result),
|
|
Self::CloseTab(cmd) => tab::close::context(cmd, result),
|
|
Self::Goto(cmd) => navigation::goto::context(cmd, result),
|
|
Self::BatchSnapshot(cmd) => observation::batch_snapshot::context(cmd, result),
|
|
Self::Snapshot(cmd) => observation::snapshot::context(cmd, result),
|
|
Self::Title(cmd) => observation::title::context(cmd, result),
|
|
Self::Url(cmd) => observation::url::context(cmd, result),
|
|
Self::Viewport(cmd) => observation::viewport::context(cmd, result),
|
|
Self::Html(cmd) => observation::html::context(cmd, result),
|
|
Self::Text(cmd) => observation::text::context(cmd, result),
|
|
Self::Value(cmd) => observation::value::context(cmd, result),
|
|
Self::Attr(cmd) => observation::attr::context(cmd, result),
|
|
Self::Attrs(cmd) => observation::attrs::context(cmd, result),
|
|
Self::Box(cmd) => observation::r#box::context(cmd, result),
|
|
Self::Styles(cmd) => observation::styles::context(cmd, result),
|
|
Self::Describe(cmd) => observation::describe::context(cmd, result),
|
|
Self::State(cmd) => observation::state::context(cmd, result),
|
|
Self::Query(cmd) => observation::query::context(cmd, result),
|
|
Self::InspectPoint(cmd) => observation::inspect_point::context(cmd, result),
|
|
Self::Pdf(cmd) => observation::pdf::context(cmd, result),
|
|
Self::Cookies { command } => match command {
|
|
CookiesCommands::List(cmd) => cookies::list::context(cmd, result),
|
|
CookiesCommands::Get(cmd) => cookies::get::context(cmd, result),
|
|
CookiesCommands::Set(cmd) => cookies::set::context(cmd, result),
|
|
CookiesCommands::Delete(cmd) => cookies::delete::context(cmd, result),
|
|
CookiesCommands::Clear(cmd) => cookies::clear::context(cmd, result),
|
|
},
|
|
Self::LocalStorage { command } => {
|
|
storage_context(command, storage::StorageKind::Local, result)
|
|
}
|
|
Self::SessionStorage { command } => {
|
|
storage_context(command, storage::StorageKind::Session, result)
|
|
}
|
|
Self::Logs { command } => match command {
|
|
LogsCommands::Console(cmd) => observation::logs_console::context(cmd, result),
|
|
LogsCommands::Errors(cmd) => observation::logs_errors::context(cmd, result),
|
|
LogsCommands::Daemon(_) => None,
|
|
},
|
|
Self::Network { command } => match command {
|
|
NetworkCommands::Requests(cmd) => {
|
|
observation::network_requests::context(cmd, result)
|
|
}
|
|
NetworkCommands::Request(cmd) => {
|
|
observation::network_request_detail::context(cmd, result)
|
|
}
|
|
NetworkCommands::Har { command } => match command {
|
|
HarCommands::Start(cmd) => observation::network_har::start_context(cmd, result),
|
|
HarCommands::Stop(cmd) => observation::network_har::stop_context(cmd, result),
|
|
},
|
|
},
|
|
Self::Wait { command } => match command {
|
|
WaitCommands::Element(cmd) => wait::element::context(cmd, result),
|
|
WaitCommands::Navigation(cmd) => wait::navigation::context(cmd, result),
|
|
WaitCommands::NetworkIdle(cmd) => wait::network_idle::context(cmd, result),
|
|
WaitCommands::Condition(cmd) => wait::condition::context(cmd, result),
|
|
},
|
|
Self::Eval(cmd) => interaction::eval::context(cmd, result),
|
|
Self::Back(a) => navigation::back::context(
|
|
&navigation::back::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
},
|
|
result,
|
|
),
|
|
Self::Forward(a) => navigation::forward::context(
|
|
&navigation::forward::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
},
|
|
result,
|
|
),
|
|
Self::Reload(a) => navigation::reload::context(
|
|
&navigation::reload::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
},
|
|
result,
|
|
),
|
|
Self::Click(cmd) => interaction::click::context(cmd, result),
|
|
Self::BatchClick(cmd) => interaction::batch_click::context(cmd, result),
|
|
Self::Hover(cmd) => interaction::hover::context(cmd, result),
|
|
Self::Focus(cmd) => interaction::focus::context(cmd, result),
|
|
Self::Press(cmd) => interaction::press::context(cmd, result),
|
|
Self::Type(cmd) => interaction::type_text::context(cmd, result),
|
|
Self::Fill(cmd) => interaction::fill::context(cmd, result),
|
|
Self::Select(cmd) => interaction::select::context(cmd, result),
|
|
Self::Drag(cmd) => interaction::drag::context(cmd, result),
|
|
Self::Upload(cmd) => interaction::upload::context(cmd, result),
|
|
Self::MouseMove(cmd) => interaction::mouse_move::context(cmd, result),
|
|
Self::CursorPosition(cmd) => interaction::cursor_position::context(cmd, result),
|
|
Self::Scroll(cmd) => interaction::scroll::context(cmd, result),
|
|
Self::Screenshot(cmd) => observation::screenshot::context(cmd, result),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Build an `Action` from storage subcommand args + kind.
|
|
fn storage_to_action(cmd: &StorageSubCommands, kind: storage::StorageKind) -> Action {
|
|
match cmd {
|
|
StorageSubCommands::List(a) => Action::StorageList(storage::list::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
}),
|
|
StorageSubCommands::Get(a) => Action::StorageGet(storage::get::Cmd {
|
|
key: a.key.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
}),
|
|
StorageSubCommands::Set(a) => Action::StorageSet(storage::set::Cmd {
|
|
key: a.key.clone(),
|
|
value: a.value.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
}),
|
|
StorageSubCommands::Delete(a) => Action::StorageDelete(storage::delete::Cmd {
|
|
key: a.key.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
}),
|
|
StorageSubCommands::Clear(a) => Action::StorageClear(storage::clear::Cmd {
|
|
key: a.key.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Return the command name string for a storage subcommand + kind.
|
|
fn storage_command_name(cmd: &StorageSubCommands, kind: storage::StorageKind) -> &'static str {
|
|
match cmd {
|
|
StorageSubCommands::List(_) => storage::list::command_name(kind),
|
|
StorageSubCommands::Get(_) => storage::get::command_name(kind),
|
|
StorageSubCommands::Set(_) => storage::set::command_name(kind),
|
|
StorageSubCommands::Delete(_) => storage::delete::command_name(kind),
|
|
StorageSubCommands::Clear(_) => storage::clear::command_name(kind),
|
|
}
|
|
}
|
|
|
|
/// Build response context for a storage subcommand.
|
|
fn storage_context(
|
|
cmd: &StorageSubCommands,
|
|
kind: storage::StorageKind,
|
|
result: &ActionResult,
|
|
) -> Option<ResponseContext> {
|
|
match cmd {
|
|
StorageSubCommands::List(a) => storage::list::context(
|
|
&storage::list::Cmd {
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
},
|
|
result,
|
|
),
|
|
StorageSubCommands::Get(a) => storage::get::context(
|
|
&storage::get::Cmd {
|
|
key: a.key.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
},
|
|
result,
|
|
),
|
|
StorageSubCommands::Set(a) => storage::set::context(
|
|
&storage::set::Cmd {
|
|
key: a.key.clone(),
|
|
value: a.value.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
},
|
|
result,
|
|
),
|
|
StorageSubCommands::Delete(a) => storage::delete::context(
|
|
&storage::delete::Cmd {
|
|
key: a.key.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
},
|
|
result,
|
|
),
|
|
StorageSubCommands::Clear(a) => storage::clear::context(
|
|
&storage::clear::Cmd {
|
|
key: a.key.clone(),
|
|
session: a.session.clone(),
|
|
tab: a.tab.clone(),
|
|
kind,
|
|
},
|
|
result,
|
|
),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_setup_target_only_flags() {
|
|
let cli = Cli::try_parse_from(["actionbook", "setup", "--target", "codex"])
|
|
.expect("parse setup target-only");
|
|
|
|
match cli.command {
|
|
Some(Commands::Setup(cmd)) => {
|
|
assert_eq!(cmd.target, Some(setup::skills::SetupTarget::Codex));
|
|
assert!(cmd.api_key.is_none());
|
|
assert!(cmd.browser.is_none());
|
|
assert!(!cmd.non_interactive);
|
|
assert!(!cmd.reset);
|
|
}
|
|
other => panic!("expected setup command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_setup_target_short_flag() {
|
|
let cli = Cli::try_parse_from(["actionbook", "setup", "-t", "claude"])
|
|
.expect("parse setup quick mode");
|
|
|
|
match cli.command {
|
|
Some(Commands::Setup(cmd)) => {
|
|
assert_eq!(cmd.target, Some(setup::skills::SetupTarget::Claude));
|
|
assert!(cmd.api_key.is_none());
|
|
assert!(!cmd.non_interactive);
|
|
}
|
|
other => panic!("expected setup command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_rejects_unknown_setup_target() {
|
|
let result = Cli::try_parse_from(["actionbook", "setup", "--target", "vim"]);
|
|
assert!(result.is_err(), "expected clap to reject unknown target");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_rejects_setup_target_with_api_key() {
|
|
let result = Cli::try_parse_from([
|
|
"actionbook",
|
|
"setup",
|
|
"--target",
|
|
"codex",
|
|
"--api-key",
|
|
"sk",
|
|
]);
|
|
assert!(result.is_err(), "expected clap to reject conflicting flags");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_rejects_setup_target_with_browser() {
|
|
let result = Cli::try_parse_from([
|
|
"actionbook",
|
|
"setup",
|
|
"--target",
|
|
"codex",
|
|
"--browser",
|
|
"local",
|
|
]);
|
|
assert!(result.is_err(), "expected clap to reject conflicting flags");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_rejects_setup_target_with_reset() {
|
|
let result = Cli::try_parse_from(["actionbook", "setup", "--target", "codex", "--reset"]);
|
|
assert!(result.is_err(), "expected clap to reject conflicting flags");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_search_query() {
|
|
let cli =
|
|
Cli::try_parse_from(["actionbook", "search", "query text"]).expect("parse search");
|
|
|
|
assert!(!cli.json);
|
|
match cli.command {
|
|
Some(Commands::Search { keyword }) => {
|
|
assert_eq!(keyword, "query text");
|
|
}
|
|
other => panic!("expected search command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_search_query_with_json_flag() {
|
|
let cli = Cli::try_parse_from(["actionbook", "search", "query", "--json"])
|
|
.expect("parse search --json");
|
|
|
|
assert!(cli.json);
|
|
match cli.command {
|
|
Some(Commands::Search { keyword }) => {
|
|
assert_eq!(keyword, "query");
|
|
}
|
|
other => panic!("expected search command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_manual_site_only() {
|
|
let cli = Cli::try_parse_from(["actionbook", "manual", "notion"]).expect("parse manual");
|
|
|
|
assert!(!cli.json);
|
|
match cli.command {
|
|
Some(Commands::Manual {
|
|
site,
|
|
group,
|
|
action,
|
|
}) => {
|
|
assert_eq!(site.as_deref(), Some("notion"));
|
|
assert!(group.is_none());
|
|
assert!(action.is_none());
|
|
}
|
|
other => panic!("expected manual command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_manual_alias_with_full_path_and_json_flag() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"man",
|
|
"notion",
|
|
"pages",
|
|
"create_page",
|
|
"--json",
|
|
])
|
|
.expect("parse manual alias --json");
|
|
|
|
assert!(cli.json);
|
|
match cli.command {
|
|
Some(Commands::Manual {
|
|
site,
|
|
group,
|
|
action,
|
|
}) => {
|
|
assert_eq!(site.as_deref(), Some("notion"));
|
|
assert_eq!(group.as_deref(), Some("pages"));
|
|
assert_eq!(action.as_deref(), Some("create_page"));
|
|
}
|
|
other => panic!("expected manual command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_manual_with_all_args() {
|
|
let cli = Cli::try_parse_from(["actionbook", "manual", "notion", "pages", "create_page"])
|
|
.expect("parse manual with all args");
|
|
|
|
assert!(!cli.json);
|
|
match cli.command {
|
|
Some(Commands::Manual {
|
|
site,
|
|
group,
|
|
action,
|
|
}) => {
|
|
assert_eq!(site.as_deref(), Some("notion"));
|
|
assert_eq!(group.as_deref(), Some("pages"));
|
|
assert_eq!(action.as_deref(), Some("create_page"));
|
|
}
|
|
other => panic!("expected manual command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_manual_site_only() {
|
|
let cli =
|
|
Cli::try_parse_from(["actionbook", "manual", "notion"]).expect("parse manual site");
|
|
|
|
assert!(!cli.json);
|
|
match cli.command {
|
|
Some(Commands::Manual {
|
|
site,
|
|
group,
|
|
action,
|
|
}) => {
|
|
assert_eq!(site.as_deref(), Some("notion"));
|
|
assert!(group.is_none());
|
|
assert!(action.is_none());
|
|
}
|
|
other => panic!("expected manual command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_manual_alias() {
|
|
let cli = Cli::try_parse_from(["actionbook", "man", "notion"]).expect("parse manual alias");
|
|
|
|
assert!(!cli.json);
|
|
match cli.command {
|
|
Some(Commands::Manual {
|
|
site,
|
|
group,
|
|
action,
|
|
}) => {
|
|
assert_eq!(site.as_deref(), Some("notion"));
|
|
assert!(group.is_none());
|
|
assert!(action.is_none());
|
|
}
|
|
other => panic!("expected manual command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_doctor_command() {
|
|
let cli = Cli::try_parse_from(["actionbook", "--json", "browser", "doctor", "--start"])
|
|
.expect("parse browser doctor");
|
|
|
|
assert!(cli.json);
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::Doctor(cmd),
|
|
}) => assert!(cmd.start),
|
|
other => panic!("expected browser doctor command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_logs_daemon_command() {
|
|
let cli = Cli::try_parse_from(["actionbook", "browser", "logs", "daemon", "--tail", "7"])
|
|
.expect("parse browser logs daemon");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command:
|
|
BrowserCommands::Logs {
|
|
command: LogsCommands::Daemon(cmd),
|
|
},
|
|
}) => assert_eq!(cmd.tail, 7),
|
|
other => panic!("expected browser logs daemon command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_hover_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"hover",
|
|
"#submit",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser hover command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_focus_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"focus",
|
|
"#submit",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser focus command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_press_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"press",
|
|
"Enter",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser press command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_drag_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"drag",
|
|
"#source",
|
|
"#target",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser drag command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_upload_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"upload",
|
|
"#file-input",
|
|
"/tmp/example.txt",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser upload command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_rejects_browser_upload_without_files() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"upload",
|
|
"#file-input",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(
|
|
cli.is_err(),
|
|
"browser upload should require at least one file"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_eval_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"eval",
|
|
"2 + 2",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser eval command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_eval_timeout_flag() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"eval",
|
|
"await Promise.resolve(1)",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
"--timeout",
|
|
"250",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser eval should accept --timeout");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_eval_file_flag() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"eval",
|
|
"--file",
|
|
"/tmp/script.js",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser eval should accept --file");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_mouse_move_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser mouse-move command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_cursor_position_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser cursor-position command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_scroll_direction_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"scroll",
|
|
"down",
|
|
"180",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser scroll direction command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_scroll_edge_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"scroll",
|
|
"bottom",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
"--container",
|
|
"#scroll-box",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser scroll edge command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_scroll_into_view_command() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"scroll",
|
|
"into-view",
|
|
"#target",
|
|
"--session",
|
|
"session-1",
|
|
"--tab",
|
|
"tab-1",
|
|
"--align",
|
|
"center",
|
|
]);
|
|
|
|
assert!(cli.is_ok(), "browser scroll into-view command should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_html_without_selector() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"html",
|
|
"--session",
|
|
"s1",
|
|
"--tab",
|
|
"t1",
|
|
])
|
|
.expect("parse html");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::Html(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.selector, None);
|
|
assert_eq!(cmd.session, "s1");
|
|
assert_eq!(cmd.tab, "t1");
|
|
}
|
|
other => panic!("expected browser html command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_attr_selector_and_name() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"attr",
|
|
"#email",
|
|
"aria-label",
|
|
"--session",
|
|
"s1",
|
|
"--tab",
|
|
"t1",
|
|
])
|
|
.expect("parse attr");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::Attr(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.selector, "#email");
|
|
assert_eq!(cmd.name, "aria-label");
|
|
assert_eq!(cmd.session, "s1");
|
|
assert_eq!(cmd.tab, "t1");
|
|
}
|
|
other => panic!("expected browser attr command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_parses_styles_names_after_options() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"styles",
|
|
"#target",
|
|
"--session",
|
|
"s1",
|
|
"--tab",
|
|
"t1",
|
|
"color",
|
|
"backgroundColor",
|
|
"z-index",
|
|
])
|
|
.expect("parse styles");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::Styles(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.selector, "#target");
|
|
assert_eq!(cmd.session, "s1");
|
|
assert_eq!(cmd.tab, "t1");
|
|
assert_eq!(cmd.names, vec!["color", "backgroundColor", "z-index"]);
|
|
}
|
|
other => panic!("expected browser styles command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_start_session_flag() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"start",
|
|
"--session",
|
|
"my-session",
|
|
"-p",
|
|
"hyperbrowser",
|
|
"--headless",
|
|
])
|
|
.expect("browser start --session should parse");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::Start(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.session.as_deref(), Some("my-session"));
|
|
assert_eq!(cmd.provider.as_deref(), Some("hyperbrowser"));
|
|
assert!(
|
|
cmd.set_session_id.is_none(),
|
|
"set_session_id should be None when --session is used"
|
|
);
|
|
}
|
|
other => panic!("expected browser start command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_rejects_session_and_set_session_id_together() {
|
|
let result = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"start",
|
|
"--session",
|
|
"my-session",
|
|
"--set-session-id",
|
|
"other-session",
|
|
]);
|
|
assert!(
|
|
result.is_err(),
|
|
"--session and --set-session-id should conflict"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_batch_new_tab() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"batch-new-tab",
|
|
"--urls",
|
|
"https://a.com",
|
|
"https://b.com",
|
|
"--session",
|
|
"s1",
|
|
])
|
|
.expect("batch-new-tab should parse");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::BatchNewTab(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.urls, vec!["https://a.com", "https://b.com"]);
|
|
assert!(cmd.tabs.is_empty());
|
|
assert_eq!(cmd.session, "s1");
|
|
}
|
|
other => panic!("expected browser batch-new-tab, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_batch_new_tab_with_tabs() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"batch-new-tab",
|
|
"--urls",
|
|
"https://a.com",
|
|
"https://b.com",
|
|
"--tabs",
|
|
"inbox",
|
|
"settings",
|
|
"--session",
|
|
"s1",
|
|
])
|
|
.expect("batch-new-tab with --tabs should parse");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::BatchNewTab(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.urls, vec!["https://a.com", "https://b.com"]);
|
|
assert_eq!(cmd.tabs, vec!["inbox", "settings"]);
|
|
assert_eq!(cmd.session, "s1");
|
|
}
|
|
other => panic!("expected browser batch-new-tab, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_batch_open_alias() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"batch-open",
|
|
"--urls",
|
|
"https://a.com",
|
|
"--session",
|
|
"s1",
|
|
]);
|
|
assert!(cli.is_ok(), "batch-open alias should parse");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_rejects_batch_new_tab_no_urls() {
|
|
let cli =
|
|
Cli::try_parse_from(["actionbook", "browser", "batch-new-tab", "--session", "s1"]);
|
|
assert!(cli.is_err(), "batch-new-tab without --urls should fail");
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_new_tab_tab_alias() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"new-tab",
|
|
"https://example.com",
|
|
"--session",
|
|
"s1",
|
|
"--tab",
|
|
"inbox",
|
|
])
|
|
.expect("browser new-tab --tab should parse");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::NewTab(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.urls, vec!["https://example.com"]);
|
|
assert_eq!(cmd.set_tab_id, vec!["inbox"]);
|
|
}
|
|
other => panic!("expected browser new-tab command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_new_tab_multiple_urls() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"new-tab",
|
|
"https://a.com",
|
|
"https://b.com",
|
|
"--session",
|
|
"s1",
|
|
])
|
|
.expect("browser new-tab multiple URLs should parse");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::NewTab(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.urls, vec!["https://a.com", "https://b.com"]);
|
|
assert_eq!(cmd.session, "s1");
|
|
assert!(cmd.set_tab_id.is_empty());
|
|
}
|
|
other => panic!("expected browser new-tab command, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn try_parse_from_accepts_browser_new_tab_multiple_tab_aliases() {
|
|
let cli = Cli::try_parse_from([
|
|
"actionbook",
|
|
"browser",
|
|
"new-tab",
|
|
"https://a.com",
|
|
"https://b.com",
|
|
"--session",
|
|
"s1",
|
|
"--tab",
|
|
"inbox",
|
|
"--tab",
|
|
"docs",
|
|
])
|
|
.expect("browser new-tab repeated --tab should parse");
|
|
|
|
match cli.command {
|
|
Some(Commands::Browser {
|
|
command: BrowserCommands::NewTab(cmd),
|
|
}) => {
|
|
assert_eq!(cmd.urls, vec!["https://a.com", "https://b.com"]);
|
|
assert_eq!(cmd.set_tab_id, vec!["inbox", "docs"]);
|
|
}
|
|
other => panic!("expected browser new-tab command, got {other:?}"),
|
|
}
|
|
}
|
|
}
|