use crate::render::chrome::page::Page;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::policy::{ActionPolicy, ActionRule, ActionVerb};
use crate::render::interact::{
click_selector, eval_js, scroll_by, type_text, wait_for_selector, MousePos,
};
use crate::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
WaitFor { selector: String, timeout_ms: u64 },
WaitMs { ms: u64 },
Click { selector: String },
Type { selector: String, text: String },
Scroll { dy: f64 },
Eval { script: String },
Submit { selector: String },
Press { key: String },
}
impl Action {
pub fn verb(&self) -> ActionVerb {
match self {
Action::WaitFor { .. } | Action::WaitMs { .. } => ActionVerb::Scroll,
Action::Click { .. } => ActionVerb::Click,
Action::Type { .. } => ActionVerb::Type,
Action::Scroll { .. } => ActionVerb::Scroll,
Action::Eval { .. } => ActionVerb::Eval,
Action::Submit { .. } => ActionVerb::Submit,
Action::Press { .. } => ActionVerb::Press,
}
}
}
pub async fn execute(page: &Page, script: &[Action]) -> Result<()> {
execute_with_policy(page, script, &ActionPolicy::permissive()).await
}
pub async fn execute_with_policy(
page: &Page,
script: &[Action],
policy: &ActionPolicy,
) -> Result<()> {
let mut pos = MousePos { x: 100.0, y: 100.0 };
for act in script {
let verb = act.verb();
match policy.check(verb) {
ActionRule::Allow => {}
ActionRule::Deny => {
return Err(crate::Error::HookAbort(format!(
"action_policy: {verb:?} denied",
verb = verb.as_str()
)));
}
ActionRule::Confirm => {
return Err(crate::Error::HookAbort(format!(
"action_policy: {verb:?} requires confirmation (HITL unavailable in this build)",
verb = verb.as_str()
)));
}
}
match act {
Action::WaitFor {
selector,
timeout_ms,
} => {
wait_for_selector(page, selector, *timeout_ms).await?;
}
Action::WaitMs { ms } => {
tokio::time::sleep(Duration::from_millis(*ms)).await;
}
Action::Click { selector } => {
pos = click_selector(page, selector, pos).await?;
}
Action::Type { selector, text } => {
type_text(page, selector, text).await?;
}
Action::Scroll { dy } => {
scroll_by(page, *dy, pos).await?;
}
Action::Eval { script } => {
eval_js(page, script).await?;
}
Action::Submit { selector } => {
pos = click_selector(page, selector, pos).await?;
}
Action::Press { key } => {
press_key(page, key).await?;
}
}
}
Ok(())
}
async fn press_key(page: &Page, key: &str) -> Result<()> {
use crate::render::chrome_protocol::cdp::browser_protocol::input::{
DispatchKeyEventParams, DispatchKeyEventType,
};
for ty in [DispatchKeyEventType::KeyDown, DispatchKeyEventType::KeyUp] {
let p = DispatchKeyEventParams::builder()
.r#type(ty)
.key(key.to_string())
.build()
.map_err(|e| crate::Error::Render(format!("press params: {e}")))?;
page.execute(p)
.await
.map_err(|e| crate::Error::Render(format!("press: {e}")))?;
}
Ok(())
}