use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SwipeTarget {
Target(String),
Point([f64; 2]),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "camelCase", deny_unknown_fields)]
pub enum Step {
Tap { target: String },
DoubleTap { target: String },
Drag {
from: String,
to: String,
#[serde(default, rename = "sourceHoldMs")]
source_hold_ms: Option<u64>,
#[serde(default, rename = "moveMs", alias = "ms")]
move_ms: Option<u64>,
#[serde(default, rename = "destinationHoldMs")]
destination_hold_ms: Option<u64>,
},
Pinch {
#[serde(default)]
target: Option<String>,
scale: f64,
},
Rotate {
#[serde(default)]
target: Option<String>,
degrees: f64,
},
OpenUrl { url: String },
#[serde(rename_all = "camelCase")]
Launch { bundle_id: String },
#[serde(rename_all = "camelCase")]
Terminate { bundle_id: String },
SetLocation { lat: f64, lon: f64 },
Appearance { appearance: String },
ClipboardRead,
ClipboardWrite { text: String },
KeyboardDismiss,
Back { mode: String },
AppSwitcher,
ScreenshotElement { target: String },
Alert {
op: String,
#[serde(default)]
ms: Option<u64>,
},
Pan {
#[serde(default)]
target: Option<String>,
dx: f64,
dy: f64,
#[serde(default)]
ms: Option<u64>,
},
Transform {
#[serde(default)]
target: Option<String>,
#[serde(default)]
dx: f64,
#[serde(default)]
dy: f64,
scale: f64,
#[serde(default)]
degrees: f64,
},
Fling { direction: String },
Swipe {
from: SwipeTarget,
to: SwipeTarget,
#[serde(default)]
count: Option<u32>,
#[serde(default)]
#[serde(rename = "pauseMs")]
pause_ms: Option<u64>,
},
#[serde(rename_all = "camelCase")]
LongPress {
target: String,
#[serde(default)]
ms: Option<u64>,
},
Fill {
target: String,
value: String,
#[serde(default)]
secret: bool,
},
Scroll {
direction: String,
#[serde(default)]
target: Option<String>,
},
Type { value: String },
Key { name: String },
Press { button: String },
#[serde(rename_all = "camelCase")]
WaitFor {
text: String,
#[serde(default)]
timeout_ms: Option<u64>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StepFailure {
pub index: usize,
pub step: Step,
pub code: String,
pub error: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn steps_parse_from_the_json_agents_write() {
let steps: Vec<Step> = serde_json::from_str(
r#"[{"action":"fill","target":"Email","value":"a@b.c"},
{"action":"tap","target":"e4"},
{"action":"waitFor","text":"Welcome","timeoutMs":2000},
{"action":"press","button":"home"}]"#,
)
.unwrap();
assert_eq!(
steps[0],
Step::Fill {
target: "Email".into(),
value: "a@b.c".into(),
secret: false
}
);
assert_eq!(
steps[2],
Step::WaitFor {
text: "Welcome".into(),
timeout_ms: Some(2000)
}
);
let steps: Vec<Step> = serde_json::from_str(
r#"[{"action":"longPress","target":"e4"},
{"action":"longPress","target":"General","ms":1500},
{"action":"drag","from":"e1","to":"C4"},
{"action":"drag","from":"Email","to":"Log in","ms":1200},
{"action":"pinch","scale":2.0},
{"action":"rotate","target":"Map","degrees":90},
{"action":"key","name":"cmd+a"},
{"action":"openUrl","url":"https://example.com"},
{"action":"setLocation","lat":6.5,"lon":3.3},
{"action":"appearance","appearance":"dark"},
{"action":"keyboardDismiss"},
{"action":"back","mode":"inApp"},
{"action":"screenshotElement","target":"e1"},
{"action":"launch","bundleId":"com.apple.Preferences"},
{"action":"swipe","from":[0.5,0.8],"to":"e2","count":2,"pauseMs":100},
{"action":"drag","from":"e1","to":"e2","sourceHoldMs":300,"moveMs":400,"destinationHoldMs":50}]"#,
)
.unwrap();
assert_eq!(
steps[0],
Step::LongPress {
target: "e4".into(),
ms: None
}
);
assert_eq!(
steps[1],
Step::LongPress {
target: "General".into(),
ms: Some(1500)
}
);
assert_eq!(
steps[2],
Step::Drag {
from: "e1".into(),
to: "C4".into(),
source_hold_ms: None,
move_ms: None,
destination_hold_ms: None
}
);
assert_eq!(
steps[3],
Step::Drag {
from: "Email".into(),
to: "Log in".into(),
source_hold_ms: None,
move_ms: Some(1200),
destination_hold_ms: None
}
);
assert_eq!(
steps[4],
Step::Pinch {
target: None,
scale: 2.0
}
);
assert_eq!(
steps[5],
Step::Rotate {
target: Some("Map".into()),
degrees: 90.0
}
);
assert_eq!(
steps[6],
Step::Key {
name: "cmd+a".into()
}
);
assert_eq!(
steps[7],
Step::OpenUrl {
url: "https://example.com".into()
}
);
assert_eq!(steps[8], Step::SetLocation { lat: 6.5, lon: 3.3 });
assert_eq!(
steps[9],
Step::Appearance {
appearance: "dark".into()
}
);
assert_eq!(steps[10], Step::KeyboardDismiss);
assert_eq!(
steps[11],
Step::Back {
mode: "inApp".into()
}
);
assert_eq!(
steps[12],
Step::ScreenshotElement {
target: "e1".into()
}
);
assert_eq!(
steps[13],
Step::Launch {
bundle_id: "com.apple.Preferences".into()
}
);
assert_eq!(
steps[14],
Step::Swipe {
from: SwipeTarget::Point([0.5, 0.8]),
to: SwipeTarget::Target("e2".into()),
count: Some(2),
pause_ms: Some(100)
}
);
assert_eq!(
steps[15],
Step::Drag {
from: "e1".into(),
to: "e2".into(),
source_hold_ms: Some(300),
move_ms: Some(400),
destination_hold_ms: Some(50)
}
);
assert!(
serde_json::from_str::<Step>(r#"{"action":"launch","bundle_id":"x"}"#).is_err(),
"camelCase only"
);
assert!(
serde_json::from_str::<Step>(r#"{"action":"tap","targt":"x"}"#).is_err(),
"typos are errors"
);
}
}