mod common;
use std::io::Write;
use std::time::Duration;
use wdotool_core::backend::mock::BackendCall;
use wdotool_core::types::KeyDirection;
use wdotool_core::WdoError;
#[tokio::test]
async fn key_plain_letter_records_press_release() {
let r = common::run(&["key", "a"]).await;
assert!(r.exit.is_success());
assert_eq!(
r.calls,
vec![BackendCall::Key {
keysym: "a".into(),
dir: KeyDirection::PressRelease,
}]
);
}
#[tokio::test]
async fn key_with_modifier_chain_uses_press_then_press_release_then_release() {
let r = common::run(&["key", "ctrl+shift+a"]).await;
assert!(r.exit.is_success());
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 keydown_only_presses() {
let r = common::run(&["keydown", "ctrl+a"]).await;
assert!(r.exit.is_success());
assert_eq!(
r.calls,
vec![
BackendCall::Key {
keysym: "Control_L".into(),
dir: KeyDirection::Press,
},
BackendCall::Key {
keysym: "a".into(),
dir: KeyDirection::Press,
},
]
);
}
#[tokio::test]
async fn keyup_only_releases_in_reverse() {
let r = common::run(&["keyup", "ctrl+shift+a"]).await;
assert!(r.exit.is_success());
assert_eq!(
r.calls,
vec![
BackendCall::Key {
keysym: "a".into(),
dir: KeyDirection::Release,
},
BackendCall::Key {
keysym: "Shift_L".into(),
dir: KeyDirection::Release,
},
BackendCall::Key {
keysym: "Control_L".into(),
dir: KeyDirection::Release,
},
]
);
}
#[tokio::test]
async fn key_clearmodifiers_releases_standard_set_first() {
let r = common::run(&["key", "--clearmodifiers", "a"]).await;
assert!(r.exit.is_success());
let expected_modifiers = [
"Control_L",
"Control_R",
"Shift_L",
"Shift_R",
"Alt_L",
"Alt_R",
"Super_L",
"Super_R",
"ISO_Level3_Shift",
];
for (i, sym) in expected_modifiers.iter().enumerate() {
assert_eq!(
r.calls[i],
BackendCall::Key {
keysym: (*sym).into(),
dir: KeyDirection::Release,
},
"modifier {i} ({sym}) was not released first"
);
}
assert_eq!(
r.calls[expected_modifiers.len()],
BackendCall::Key {
keysym: "a".into(),
dir: KeyDirection::PressRelease,
}
);
assert_eq!(r.calls.len(), expected_modifiers.len() + 1);
}
#[tokio::test]
async fn type_records_text_with_default_delay_of_12ms() {
let r = common::run(&["type", "hello"]).await;
assert!(r.exit.is_success());
assert_eq!(
r.calls,
vec![BackendCall::TypeText {
text: "hello".into(),
delay: Duration::from_millis(12),
}]
);
}
#[tokio::test]
async fn type_honors_explicit_delay() {
let r = common::run(&["type", "--delay", "50", "hi"]).await;
assert!(r.exit.is_success());
assert_eq!(
r.calls,
vec![BackendCall::TypeText {
text: "hi".into(),
delay: Duration::from_millis(50),
}]
);
}
#[tokio::test]
async fn type_passes_multibyte_utf8_through_unchanged() {
let text = "hello 世界 🌍";
let r = common::run(&["type", text]).await;
assert!(r.exit.is_success());
assert_eq!(
r.calls,
vec![BackendCall::TypeText {
text: text.into(),
delay: Duration::from_millis(12),
}]
);
}
#[tokio::test]
async fn type_clearmodifiers_releases_first_then_types() {
let r = common::run(&["type", "--clearmodifiers", "x"]).await;
assert!(r.exit.is_success());
let last = r.calls.last().unwrap();
assert_eq!(
last,
&BackendCall::TypeText {
text: "x".into(),
delay: Duration::from_millis(12),
}
);
for (i, c) in r.calls[..r.calls.len() - 1].iter().enumerate() {
match c {
BackendCall::Key {
dir: KeyDirection::Release,
..
} => {}
other => panic!("call {i} is not a Release: {other:?}"),
}
}
}
#[tokio::test]
async fn type_reads_from_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("input.txt");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(b"from-file\n").unwrap();
drop(f);
let r = common::run(&["type", "--file", path.to_str().unwrap()]).await;
assert!(r.exit.is_success(), "stderr: {}", r.stderr);
assert_eq!(
r.calls,
vec![BackendCall::TypeText {
text: "from-file\n".into(),
delay: Duration::from_millis(12),
}]
);
}
#[tokio::test]
async fn type_with_neither_arg_errors() {
let r = common::run(&["type"]).await;
let err = r
.error
.expect("expected an error from `type` with no input");
assert!(matches!(err, WdoError::InvalidArg(_)), "got: {err:?}");
}
#[tokio::test]
async fn key_with_invalid_chain_errors_before_calling_backend() {
let r = common::run(&["key", ""]).await;
assert!(r.error.is_some(), "expected an error for empty chain");
assert!(
r.calls.is_empty(),
"no backend calls should fire on parse failure: got {:?}",
r.calls
);
}
#[tokio::test]
async fn key_with_unknown_keysym_passes_through_to_backend() {
let r = common::run(&["key", "ThisIsNotARealKeysym1234"]).await;
assert!(r.exit.is_success(), "error: {:?}", r.error);
assert_eq!(
r.calls,
vec![BackendCall::Key {
keysym: "ThisIsNotARealKeysym1234".into(),
dir: KeyDirection::PressRelease,
}]
);
}