mod common;
use std::io::Write;
use std::time::{Duration, Instant};
use wdotool_core::backend::mock::BackendCall;
use wdotool_core::types::{KeyDirection, MouseButton};
use wdotool_core::WdoError;
fn write_trace(json: &str) -> (String, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("trace.json");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(json.as_bytes()).unwrap();
drop(f);
(path.to_string_lossy().into_owned(), dir)
}
#[tokio::test]
async fn replay_dispatches_key_event() {
let (path, _dir) = write_trace(r#"[{"kind":"key","t_ms":0,"chord":"a"}]"#);
let r = common::run(&["replay", &path]).await;
assert!(r.exit.is_success(), "error: {:?}", r.error);
assert_eq!(
r.calls,
vec![BackendCall::Key {
keysym: "a".into(),
dir: KeyDirection::PressRelease,
}]
);
}
#[tokio::test]
async fn replay_dispatches_chord_with_modifier_ordering() {
let (path, _dir) = write_trace(r#"[{"kind":"key","t_ms":0,"chord":"ctrl+shift+a"}]"#);
let r = common::run(&["replay", &path]).await;
assert!(r.exit.is_success(), "error: {:?}", r.error);
assert_eq!(
r.calls,
vec![
BackendCall::Key {
keysym: "Control_L".into(),
dir: KeyDirection::Press
},
BackendCall::Key {
keysym: "Shift_L".into(),
dir: KeyDirection::Press
},
BackendCall::Key {
keysym: "a".into(),
dir: KeyDirection::PressRelease
},
BackendCall::Key {
keysym: "Shift_L".into(),
dir: KeyDirection::Release
},
BackendCall::Key {
keysym: "Control_L".into(),
dir: KeyDirection::Release
},
]
);
}
#[tokio::test]
async fn replay_dispatches_click_as_press_release_pair() {
let (path, _dir) = write_trace(r#"[{"kind":"click","t_ms":0,"button":1}]"#);
let r = common::run(&["replay", &path]).await;
assert!(r.exit.is_success());
assert_eq!(
r.calls,
vec![BackendCall::MouseButton {
btn: MouseButton::Left,
dir: KeyDirection::PressRelease,
}]
);
}
#[tokio::test]
async fn replay_dispatches_move_abs_and_move_delta() {
let (path, _dir) = write_trace(
r#"[
{"kind":"move_abs","t_ms":0,"x":100,"y":200},
{"kind":"move_delta","t_ms":1,"dx":-5,"dy":7}
]"#,
);
let r = common::run(&["replay", &path]).await;
assert!(r.exit.is_success(), "error: {:?}", r.error);
assert_eq!(
r.calls,
vec![
BackendCall::MouseMove {
x: 100,
y: 200,
absolute: true,
},
BackendCall::MouseMove {
x: -5,
y: 7,
absolute: false,
},
]
);
}
#[tokio::test]
async fn replay_dispatches_scroll() {
let (path, _dir) = write_trace(r#"[{"kind":"scroll","t_ms":0,"dx":0,"dy":3}]"#);
let r = common::run(&["replay", &path]).await;
assert!(r.exit.is_success());
assert_eq!(r.calls, vec![BackendCall::Scroll { dx: 0.0, dy: 3.0 }]);
}
#[tokio::test]
async fn replay_sleeps_on_gap_events() {
let (path, _dir) = write_trace(
r#"[
{"kind":"key","t_ms":0,"chord":"a"},
{"kind":"gap","t_ms":1,"ms":200},
{"kind":"key","t_ms":201,"chord":"b"}
]"#,
);
let start = Instant::now();
let r = common::run(&["replay", &path]).await;
let elapsed = start.elapsed();
assert!(r.exit.is_success());
assert!(
elapsed >= Duration::from_millis(150),
"expected replay to sleep on Gap, but it took only {elapsed:?}"
);
assert_eq!(r.calls.len(), 2);
}
#[tokio::test]
async fn replay_speed_flag_scales_gap_durations() {
let (path, _dir) = write_trace(
r#"[
{"kind":"key","t_ms":0,"chord":"a"},
{"kind":"gap","t_ms":1,"ms":200},
{"kind":"key","t_ms":201,"chord":"b"}
]"#,
);
let start = Instant::now();
let r = common::run(&["replay", "--speed", "4.0", &path]).await;
let elapsed = start.elapsed();
assert!(r.exit.is_success());
assert!(
elapsed < Duration::from_millis(150),
"expected speed=4 to skip most of the Gap, but took {elapsed:?}"
);
assert_eq!(r.calls.len(), 2);
}
#[tokio::test]
async fn replay_rejects_zero_or_negative_speed() {
let (path, _dir) = write_trace(r#"[]"#);
let r = common::run(&["replay", "--speed", "0", &path]).await;
let err = r.error.expect("expected error for speed=0");
assert!(matches!(err, WdoError::InvalidArg(_)), "got: {err:?}");
}
#[tokio::test]
async fn replay_rejects_malformed_json() {
let (path, _dir) = write_trace(r#"this is not json"#);
let r = common::run(&["replay", &path]).await;
let err = r.error.expect("expected error for malformed JSON");
match err {
WdoError::InvalidArg(msg) => {
assert!(msg.contains("RecEvent"), "msg: {msg}");
}
other => panic!("expected InvalidArg, got {other:?}"),
}
}
#[tokio::test]
async fn replay_rejects_missing_file() {
let r = common::run(&["replay", "/no/such/trace/here.json"]).await;
let err = r.error.expect("expected error for missing file");
match err {
WdoError::InvalidArg(msg) => {
assert!(msg.contains("/no/such/trace/here.json"), "msg: {msg}");
}
other => panic!("expected InvalidArg, got {other:?}"),
}
}
#[tokio::test]
async fn replay_handles_empty_trace() {
let (path, _dir) = write_trace(r#"[]"#);
let r = common::run(&["replay", &path]).await;
assert!(r.exit.is_success());
assert_eq!(r.calls.len(), 0);
}