use crate::AutomationUsage;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ActionType {
Navigate,
Click,
ClickAll(String),
ClickPoint {
x: f64,
y: f64,
},
ClickHold {
selector: String,
hold_ms: u64,
},
ClickHoldPoint {
x: f64,
y: f64,
hold_ms: u64,
},
ClickDrag {
from: String,
to: String,
modifier: Option<i64>,
},
ClickDragPoint {
from_x: f64,
from_y: f64,
to_x: f64,
to_y: f64,
modifier: Option<i64>,
},
ClickAllClickable,
Type,
Fill {
selector: String,
value: String,
},
Clear,
Select,
Check,
Scroll,
ScrollX(i32),
ScrollY(i32),
InfiniteScroll(u32),
Wait,
WaitFor(String),
WaitForWithTimeout {
selector: String,
timeout: u64,
},
WaitForNavigation,
WaitForLoad,
WaitForNetworkIdle,
WaitForNetworkAlmostIdle,
WaitForDom {
selector: Option<String>,
timeout: u32,
},
WaitForAndClick(String),
Screenshot,
Script,
KeyPress,
Hover,
DragDrop,
Submit,
Back,
Forward,
Refresh,
Extract,
ValidateChain,
Custom(String),
}
impl std::fmt::Display for ActionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Navigate => write!(f, "navigate"),
Self::Click => write!(f, "click"),
Self::ClickAll(s) => write!(f, "click_all:{}", s),
Self::ClickPoint { x, y } => write!(f, "click_point:({},{})", x, y),
Self::ClickHold { selector, hold_ms } => {
write!(f, "click_hold:{}:{}ms", selector, hold_ms)
}
Self::ClickHoldPoint { x, y, hold_ms } => {
write!(f, "click_hold_point:({},{}){}ms", x, y, hold_ms)
}
Self::ClickDrag { from, to, modifier } => {
write!(f, "click_drag:{}->{}:{:?}", from, to, modifier)
}
Self::ClickDragPoint {
from_x,
from_y,
to_x,
to_y,
modifier,
} => write!(
f,
"click_drag_point:({},{})->({},{}):{:?}",
from_x, from_y, to_x, to_y, modifier
),
Self::ClickAllClickable => write!(f, "click_all_clickable"),
Self::Type => write!(f, "type"),
Self::Fill { selector, .. } => write!(f, "fill:{}", selector),
Self::Clear => write!(f, "clear"),
Self::Select => write!(f, "select"),
Self::Check => write!(f, "check"),
Self::Scroll => write!(f, "scroll"),
Self::ScrollX(px) => write!(f, "scroll_x:{}", px),
Self::ScrollY(px) => write!(f, "scroll_y:{}", px),
Self::InfiniteScroll(n) => write!(f, "infinite_scroll:{}", n),
Self::Wait => write!(f, "wait"),
Self::WaitFor(s) => write!(f, "wait_for:{}", s),
Self::WaitForWithTimeout { selector, timeout } => {
write!(f, "wait_for_timeout:{}:{}ms", selector, timeout)
}
Self::WaitForNavigation => write!(f, "wait_for_navigation"),
Self::WaitForLoad => write!(f, "wait_for_load"),
Self::WaitForNetworkIdle => write!(f, "wait_for_network_idle"),
Self::WaitForNetworkAlmostIdle => write!(f, "wait_for_network_almost_idle"),
Self::WaitForDom { selector, timeout } => {
write!(f, "wait_for_dom:{:?}:{}ms", selector, timeout)
}
Self::WaitForAndClick(s) => write!(f, "wait_and_click:{}", s),
Self::Screenshot => write!(f, "screenshot"),
Self::Script => write!(f, "script"),
Self::KeyPress => write!(f, "keypress"),
Self::Hover => write!(f, "hover"),
Self::DragDrop => write!(f, "drag_drop"),
Self::Submit => write!(f, "submit"),
Self::Back => write!(f, "back"),
Self::Forward => write!(f, "forward"),
Self::Refresh => write!(f, "refresh"),
Self::Extract => write!(f, "extract"),
Self::ValidateChain => write!(f, "validate_chain"),
Self::Custom(name) => write!(f, "custom:{}", name),
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ActionResult {
pub success: bool,
pub action_taken: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub action_type: Option<ActionType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub screenshot: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default)]
pub usage: AutomationUsage,
pub duration_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url_before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url_after: Option<String>,
}
impl ActionResult {
pub fn success(action: impl Into<String>) -> Self {
Self {
success: true,
action_taken: action.into(),
..Default::default()
}
}
pub fn failure(action: impl Into<String>, error: impl Into<String>) -> Self {
Self {
success: false,
action_taken: action.into(),
error: Some(error.into()),
..Default::default()
}
}
pub fn with_type(mut self, action_type: ActionType) -> Self {
self.action_type = Some(action_type);
self
}
pub fn with_screenshot(mut self, screenshot: impl Into<String>) -> Self {
self.screenshot = Some(screenshot.into());
self
}
pub fn with_selector(mut self, selector: impl Into<String>) -> Self {
self.selector = Some(selector.into());
self
}
pub fn with_duration(mut self, ms: u64) -> Self {
self.duration_ms = ms;
self
}
pub fn with_usage(mut self, usage: AutomationUsage) -> Self {
self.usage = usage;
self
}
pub fn with_urls(mut self, before: impl Into<String>, after: impl Into<String>) -> Self {
self.url_before = Some(before.into());
self.url_after = Some(after.into());
self
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ActionRecord {
pub step: usize,
pub action: String,
pub success: bool,
pub duration_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub url_before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url_after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub retries: usize,
}
impl ActionRecord {
pub fn new(step: usize, action: impl Into<String>, success: bool) -> Self {
Self {
step,
action: action.into(),
success,
..Default::default()
}
}
pub fn from_result(step: usize, result: &ActionResult) -> Self {
Self {
step,
action: result.action_taken.clone(),
success: result.success,
duration_ms: result.duration_ms,
url_before: result.url_before.clone(),
url_after: result.url_after.clone(),
error: result.error.clone(),
retries: 0,
}
}
pub fn with_retries(mut self, retries: usize) -> Self {
self.retries = retries;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_result() {
let result = ActionResult::success("Clicked login button")
.with_type(ActionType::Click)
.with_selector("button.login")
.with_duration(150);
assert!(result.success);
assert_eq!(result.action_type, Some(ActionType::Click));
assert_eq!(result.selector, Some("button.login".to_string()));
}
#[test]
fn test_action_record() {
let result = ActionResult::success("Typed email").with_duration(100);
let record = ActionRecord::from_result(1, &result);
assert_eq!(record.step, 1);
assert!(record.success);
assert_eq!(record.duration_ms, 100);
}
#[test]
fn test_action_type_display() {
assert_eq!(ActionType::Click.to_string(), "click");
assert_eq!(ActionType::Navigate.to_string(), "navigate");
assert_eq!(
ActionType::Custom("foo".to_string()).to_string(),
"custom:foo"
);
assert_eq!(
ActionType::ClickAll("button".to_string()).to_string(),
"click_all:button"
);
assert_eq!(ActionType::ScrollX(100).to_string(), "scroll_x:100");
assert_eq!(ActionType::ScrollY(-50).to_string(), "scroll_y:-50");
assert_eq!(
ActionType::InfiniteScroll(5).to_string(),
"infinite_scroll:5"
);
assert_eq!(
ActionType::WaitFor(".modal".to_string()).to_string(),
"wait_for:.modal"
);
assert_eq!(ActionType::ValidateChain.to_string(), "validate_chain");
}
}