use std::time::Duration;
use crate::step::Step;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum StartupWait {
Default,
FastNative,
SlowNode,
Custom {
stable_ms: u32,
timeout_ms: u32,
},
}
impl StartupWait {
pub const fn stable_ms(self) -> u32 {
match self {
Self::Default => 300,
Self::FastNative => 200,
Self::SlowNode => 500,
Self::Custom { stable_ms, .. } => stable_ms,
}
}
pub const fn timeout_ms(self) -> u32 {
match self {
Self::Default => 5_000,
Self::FastNative => 3_000,
Self::SlowNode => 10_000,
Self::Custom { timeout_ms, .. } => timeout_ms,
}
}
}
#[must_use]
#[derive(Debug, Clone)]
pub struct Journey {
pub name: String,
pub description: Option<String>,
pub steps: Vec<Step>,
}
impl Journey {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
steps: Vec::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn step(mut self, step: Step) -> Self {
self.steps.push(step);
self
}
pub fn wait_for_startup(stable_ms: u32, timeout_ms: u32) -> Self {
Self::wait_for_startup_preset(StartupWait::Custom {
stable_ms,
timeout_ms,
})
}
pub fn wait_for_startup_preset(preset: StartupWait) -> Self {
Self::new("wait_for_startup")
.with_description("Wait for application startup and frame stabilization")
.step(Step::wait_for_stable_frame(
preset.stable_ms(),
preset.timeout_ms(),
))
}
pub fn wait_for_startup_default() -> Self {
Self::wait_for_startup_preset(StartupWait::Default)
}
pub fn navigate_with_key(
key: impl Into<String>,
expected_text: impl Into<String>,
timeout_ms: u32,
) -> Self {
let key_name = key.into();
let expected = expected_text.into();
Self::new(format!("navigate_{key_name}"))
.with_description(format!("Press '{key_name}' and wait for '{expected}'"))
.step(Step::press_key(&key_name))
.step(Step::wait_for_text(expected, timeout_ms))
}
pub fn type_and_confirm(text: impl Into<String>) -> Self {
let input_text = text.into();
Self::new("type_and_confirm")
.with_description(format!("Type '{input_text}' and press Enter"))
.step(Step::write_text(&input_text))
.step(Step::press_key("Enter"))
}
pub fn press_and_wait(key: impl Into<String>, wait_ms: u64) -> Self {
let key_name = key.into();
Self::new(format!("press_{key_name}"))
.with_description(format!("Press '{key_name}' and wait {wait_ms}ms"))
.step(Step::press_key(&key_name))
.step(Step::sleep(Duration::from_millis(wait_ms)))
}
pub fn capture_labeled(label: impl Into<String>, description: impl Into<String>) -> Self {
let label_text = label.into();
let description_text = description.into();
Self::new(format!("capture_{label_text}"))
.with_description(format!("Capture: {description_text}"))
.step(Step::capture_labeled(label_text, description_text))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn journey_new_creates_empty() {
let journey = Journey::new("test_journey");
assert_eq!(journey.name, "test_journey");
assert!(journey.description.is_none());
assert!(journey.steps.is_empty());
}
#[test]
fn journey_with_description_sets_description() {
let journey = Journey::new("described").with_description("Does something");
assert_eq!(journey.description.as_deref(), Some("Does something"));
}
#[test]
fn journey_step_appends() {
let journey = Journey::new("custom")
.step(Step::write_text("hello"))
.step(Step::press_key("Enter"));
assert_eq!(journey.steps.len(), 2);
}
#[test]
fn wait_for_startup_produces_stable_frame_step() {
let journey = Journey::wait_for_startup(300, 5000);
assert_eq!(journey.name, "wait_for_startup");
assert_eq!(journey.steps.len(), 1);
assert!(matches!(
&journey.steps[0],
Step::WaitForStableFrame {
stable_ms: 300,
timeout_ms: 5000
}
));
}
#[test]
fn startup_wait_presets_expose_documented_durations() {
assert_eq!(StartupWait::Default.stable_ms(), 300);
assert_eq!(StartupWait::Default.timeout_ms(), 5_000);
assert_eq!(StartupWait::FastNative.stable_ms(), 200);
assert_eq!(StartupWait::FastNative.timeout_ms(), 3_000);
assert_eq!(StartupWait::SlowNode.stable_ms(), 500);
assert_eq!(StartupWait::SlowNode.timeout_ms(), 10_000);
let custom = StartupWait::Custom {
stable_ms: 123,
timeout_ms: 4_567,
};
assert_eq!(custom.stable_ms(), 123);
assert_eq!(custom.timeout_ms(), 4_567);
}
#[test]
fn wait_for_startup_preset_uses_preset_durations() {
let journey = Journey::wait_for_startup_preset(StartupWait::SlowNode);
assert_eq!(journey.name, "wait_for_startup");
assert_eq!(journey.steps.len(), 1);
assert!(matches!(
&journey.steps[0],
Step::WaitForStableFrame {
stable_ms: 500,
timeout_ms: 10_000,
}
));
}
#[test]
fn wait_for_startup_default_matches_default_preset() {
let from_helper = Journey::wait_for_startup_default();
let from_preset = Journey::wait_for_startup_preset(StartupWait::Default);
assert_eq!(from_helper.steps.len(), 1);
assert_eq!(from_preset.steps.len(), 1);
assert!(matches!(
&from_helper.steps[0],
Step::WaitForStableFrame {
stable_ms: 300,
timeout_ms: 5_000,
}
));
assert!(matches!(
&from_preset.steps[0],
Step::WaitForStableFrame {
stable_ms: 300,
timeout_ms: 5_000,
}
));
}
#[test]
fn wait_for_startup_raw_args_route_through_custom_preset() {
let journey = Journey::wait_for_startup(150, 2_500);
assert_eq!(journey.steps.len(), 1);
assert!(matches!(
&journey.steps[0],
Step::WaitForStableFrame {
stable_ms: 150,
timeout_ms: 2_500,
}
));
}
#[test]
fn navigate_with_key_produces_press_then_wait() {
let journey = Journey::navigate_with_key("Tab", "Sessions", 3000);
assert_eq!(journey.name, "navigate_Tab");
assert_eq!(journey.steps.len(), 2);
assert!(matches!(&journey.steps[0], Step::PressKey(key) if key == "Tab"));
assert!(
matches!(&journey.steps[1], Step::WaitForText { needle, timeout_ms: 3000 } if needle == "Sessions")
);
}
#[test]
fn type_and_confirm_produces_write_then_enter() {
let journey = Journey::type_and_confirm("hello world");
assert_eq!(journey.name, "type_and_confirm");
assert_eq!(journey.steps.len(), 2);
assert!(matches!(&journey.steps[0], Step::WriteText(text) if text == "hello world"));
assert!(matches!(&journey.steps[1], Step::PressKey(key) if key == "Enter"));
}
#[test]
fn press_and_wait_produces_key_then_sleep() {
let journey = Journey::press_and_wait("Escape", 200);
assert_eq!(journey.name, "press_Escape");
assert_eq!(journey.steps.len(), 2);
assert!(matches!(&journey.steps[0], Step::PressKey(key) if key == "Escape"));
assert!(
matches!(&journey.steps[1], Step::Sleep(duration) if *duration == Duration::from_millis(200))
);
}
#[test]
fn capture_labeled_produces_capture_step() {
let journey = Journey::capture_labeled("state", "Current state");
assert_eq!(journey.name, "capture_state");
assert_eq!(journey.steps.len(), 1);
assert!(matches!(
&journey.steps[0],
Step::CaptureLabeled { label, description }
if label == "state" && description == "Current state"
));
}
}