use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use crate::assertion::MatchResult;
use crate::frame::TerminalFrame;
pub type FramePredicate = Arc<dyn Fn(&TerminalFrame) -> MatchResult + Send + Sync>;
#[derive(Clone)]
pub enum Step {
WriteText(String),
PressKey(String),
Sleep(Duration),
WaitForText {
needle: String,
timeout_ms: u32,
},
WaitForStableFrame {
stable_ms: u32,
timeout_ms: u32,
},
Eventually {
timeout: Duration,
poll: Duration,
predicate: FramePredicate,
},
Capture,
CaptureLabeled {
label: String,
description: String,
},
ViewingPause(Duration),
}
impl Step {
pub fn write_text(text: impl Into<String>) -> Self {
Self::WriteText(text.into())
}
pub fn press_key(key: impl Into<String>) -> Self {
Self::PressKey(key.into())
}
pub fn sleep(duration: Duration) -> Self {
Self::Sleep(duration)
}
pub fn sleep_ms(ms: u64) -> Self {
Self::Sleep(Duration::from_millis(ms))
}
pub fn wait_for_text(needle: impl Into<String>, timeout_ms: u32) -> Self {
Self::WaitForText {
needle: needle.into(),
timeout_ms,
}
}
pub fn wait_for_stable_frame(stable_ms: u32, timeout_ms: u32) -> Self {
Self::WaitForStableFrame {
stable_ms,
timeout_ms,
}
}
pub fn eventually<F>(timeout: Duration, poll: Duration, predicate: F) -> Self
where
F: Fn(&TerminalFrame) -> MatchResult + Send + Sync + 'static,
{
Self::Eventually {
timeout,
poll,
predicate: Arc::new(predicate),
}
}
pub fn capture() -> Self {
Self::Capture
}
pub fn capture_labeled(label: impl Into<String>, description: impl Into<String>) -> Self {
Self::CaptureLabeled {
label: label.into(),
description: description.into(),
}
}
pub fn viewing_pause(duration: Duration) -> Self {
Self::ViewingPause(duration)
}
pub fn viewing_pause_ms(ms: u64) -> Self {
Self::ViewingPause(Duration::from_millis(ms))
}
}
impl fmt::Debug for Step {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WriteText(text) => f.debug_tuple("WriteText").field(text).finish(),
Self::PressKey(key) => f.debug_tuple("PressKey").field(key).finish(),
Self::Sleep(duration) => f.debug_tuple("Sleep").field(duration).finish(),
Self::WaitForText { needle, timeout_ms } => f
.debug_struct("WaitForText")
.field("needle", needle)
.field("timeout_ms", timeout_ms)
.finish(),
Self::WaitForStableFrame {
stable_ms,
timeout_ms,
} => f
.debug_struct("WaitForStableFrame")
.field("stable_ms", stable_ms)
.field("timeout_ms", timeout_ms)
.finish(),
Self::Eventually { timeout, poll, .. } => f
.debug_struct("Eventually")
.field("timeout", timeout)
.field("poll", poll)
.field("predicate", &"<frame predicate>")
.finish(),
Self::Capture => f.debug_tuple("Capture").finish(),
Self::CaptureLabeled { label, description } => f
.debug_struct("CaptureLabeled")
.field("label", label)
.field("description", description)
.finish(),
Self::ViewingPause(duration) => f.debug_tuple("ViewingPause").field(duration).finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_text_stores_content() {
let step = Step::write_text("hello");
let Step::WriteText(text) = step else {
unreachable!("Expected WriteText variant");
};
assert_eq!(text, "hello");
}
#[test]
fn sleep_ms_converts_to_duration() {
let step = Step::sleep_ms(500);
let Step::Sleep(duration) = step else {
unreachable!("Expected Sleep variant");
};
assert_eq!(duration, Duration::from_millis(500));
}
#[test]
fn capture_labeled_stores_label_and_description() {
let step = Step::capture_labeled("startup", "App launched");
let Step::CaptureLabeled { label, description } = step else {
unreachable!("Expected CaptureLabeled variant");
};
assert_eq!(label, "startup");
assert_eq!(description, "App launched");
}
#[test]
fn wait_for_text_stores_needle_and_timeout() {
let step = Step::wait_for_text("Loading", 5000);
let Step::WaitForText { needle, timeout_ms } = step else {
unreachable!("Expected WaitForText variant");
};
assert_eq!(needle, "Loading");
assert_eq!(timeout_ms, 5000);
}
#[test]
fn viewing_pause_stores_duration() {
let step = Step::viewing_pause(Duration::from_secs(2));
let Step::ViewingPause(duration) = step else {
unreachable!("Expected ViewingPause variant");
};
assert_eq!(duration, Duration::from_secs(2));
}
#[test]
fn viewing_pause_ms_converts_to_duration() {
let step = Step::viewing_pause_ms(1500);
let Step::ViewingPause(duration) = step else {
unreachable!("Expected ViewingPause variant");
};
assert_eq!(duration, Duration::from_millis(1500));
}
#[test]
fn eventually_stores_timeout_poll_and_callable_predicate() {
let step = Step::eventually(
Duration::from_secs(5),
Duration::from_millis(100),
|frame: &TerminalFrame| {
if frame.all_text().contains("Ready") {
Ok(())
} else {
Err(Box::new(crate::assertion::AssertionFailure {
message: "missing Ready".to_string(),
expected: crate::assertion::Expected::TextInRegion {
needle: "Ready".to_string(),
},
region: None,
matched_spans: Vec::new(),
frame_excerpt: String::new(),
}))
}
},
);
let Step::Eventually {
timeout,
poll,
predicate,
} = step
else {
unreachable!("Expected Eventually variant");
};
assert_eq!(timeout, Duration::from_secs(5));
assert_eq!(poll, Duration::from_millis(100));
let ready_frame = TerminalFrame::new(80, 24, b"Ready");
let blank_frame = TerminalFrame::new(80, 24, b"");
assert!(predicate(&ready_frame).is_ok());
assert!(predicate(&blank_frame).is_err());
}
#[test]
fn eventually_debug_redacts_predicate_body() {
let step = Step::eventually(
Duration::from_millis(250),
Duration::from_millis(25),
|_| Ok(()),
);
let rendered = format!("{step:?}");
assert!(rendered.contains("Eventually"));
assert!(rendered.contains("frame predicate"));
}
}