use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::journey::Journey;
use crate::proof::report::ProofReport;
use crate::session::{PtySession, PtySessionBuilder, PtySessionError};
use crate::step::Step;
use crate::vhs::VhsTape;
#[must_use]
pub struct Scenario {
pub name: String,
pub steps: Vec<Step>,
}
impl Scenario {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
steps: Vec::new(),
}
}
pub fn step(mut self, step: Step) -> Self {
self.steps.push(step);
self
}
pub fn write_text(self, text: impl Into<String>) -> Self {
self.step(Step::write_text(text))
}
pub fn press_key(self, key: impl Into<String>) -> Self {
self.step(Step::press_key(key))
}
pub fn sleep(self, duration: Duration) -> Self {
self.step(Step::sleep(duration))
}
pub fn sleep_ms(self, ms: u64) -> Self {
self.step(Step::sleep_ms(ms))
}
pub fn wait_for_text(self, needle: impl Into<String>, timeout_ms: u32) -> Self {
self.step(Step::wait_for_text(needle, timeout_ms))
}
pub fn wait_for_stable_frame(self, stable_ms: u32, timeout_ms: u32) -> Self {
self.step(Step::wait_for_stable_frame(stable_ms, timeout_ms))
}
pub fn viewing_pause(self, duration: Duration) -> Self {
self.step(Step::viewing_pause(duration))
}
pub fn viewing_pause_ms(self, ms: u64) -> Self {
self.step(Step::viewing_pause_ms(ms))
}
pub fn capture(self) -> Self {
self.step(Step::capture())
}
pub fn capture_labeled(self, label: impl Into<String>, description: impl Into<String>) -> Self {
self.step(Step::capture_labeled(label, description))
}
pub fn compose(mut self, journey: &Journey) -> Self {
self.steps.extend(journey.steps.iter().cloned());
self
}
pub fn execute_in_pty(
&self,
session: &mut PtySession,
) -> Result<crate::frame::TerminalFrame, PtySessionError> {
session.execute_steps(&self.steps)
}
pub fn execute_in_pty_with_proof(
&self,
session: &mut PtySession,
) -> Result<(crate::frame::TerminalFrame, ProofReport), PtySessionError> {
let mut report = ProofReport::new(&self.name);
let mut last_frame = None;
for step in &self.steps {
match step {
Step::CaptureLabeled { label, description } => {
let frame = session.capture_frame();
report.add_capture(label, description, &frame);
last_frame = Some(frame);
}
_ => {
last_frame = Some(session.execute_steps(std::slice::from_ref(step))?);
}
}
}
let final_frame = last_frame.unwrap_or_else(|| session.capture_frame());
Ok((final_frame, report))
}
pub fn run(
&self,
builder: PtySessionBuilder,
) -> Result<crate::frame::TerminalFrame, PtySessionError> {
let mut session = builder.spawn()?;
self.execute_in_pty(&mut session)
}
pub fn run_with_proof(
&self,
builder: PtySessionBuilder,
) -> Result<(crate::frame::TerminalFrame, ProofReport), PtySessionError> {
let mut session = builder.spawn()?;
self.execute_in_pty_with_proof(&mut session)
}
pub fn to_vhs_tape(
&self,
binary_path: &Path,
screenshot_path: &Path,
env_vars: &[(&str, &str)],
) -> VhsTape {
VhsTape::from_scenario(self, binary_path, screenshot_path, env_vars)
}
pub fn write_vhs_tape(
&self,
binary_path: &Path,
screenshot_path: &Path,
env_vars: &[(&str, &str)],
tape_path: &Path,
) -> Result<PathBuf, std::io::Error> {
let tape = self.to_vhs_tape(binary_path, screenshot_path, env_vars);
tape.write_to(tape_path)?;
Ok(tape_path.to_path_buf())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scenario_builder_chains_steps() {
let scenario = Scenario::new("test")
.write_text("hello")
.press_key("Enter")
.sleep_ms(100)
.capture();
assert_eq!(scenario.name, "test");
assert_eq!(scenario.steps.len(), 4);
}
#[test]
fn scenario_compiles_to_vhs_tape() {
let scenario = Scenario::new("startup").sleep_ms(500).capture();
let tape = scenario.to_vhs_tape(
Path::new("/usr/bin/echo"),
Path::new("/tmp/screenshot.png"),
&[],
);
let content = tape.render();
assert!(content.contains("Screenshot"));
assert!(content.contains("Sleep"));
}
#[test]
fn scenario_capture_labeled_adds_step() {
let scenario = Scenario::new("labeled")
.capture_labeled("init", "Initial state")
.capture_labeled("done", "Final state");
assert_eq!(scenario.steps.len(), 2);
}
#[test]
fn scenario_compose_appends_journey_steps() {
let startup = Journey::wait_for_startup(300, 5000);
let navigate = Journey::navigate_with_key("Tab", "Sessions", 3000);
let scenario = Scenario::new("composed")
.compose(&startup)
.compose(&navigate)
.capture();
assert_eq!(scenario.steps.len(), 4);
}
#[test]
fn scenario_compose_preserves_existing_steps() {
let journey = Journey::type_and_confirm("hello");
let scenario = Scenario::new("mixed")
.sleep_ms(100)
.compose(&journey)
.capture();
assert_eq!(scenario.steps.len(), 4);
}
}