#![cfg(feature = "mock-loader")]
mod common;
use autoitx::options::ShowState;
use autoitx::{Keys, Point, Selector, WinCondition, keys, recipes};
use common::Harness;
use std::time::Duration;
#[test]
fn send_passes_the_sequence_through_verbatim() {
let h = Harness::new();
h.ai.send(keys!("{CTRLDOWN}{SHIFTDOWN}j{SHIFTUP}{CTRLUP}"))
.unwrap();
assert_eq!(
h.log(),
r#"AU3_Send("{CTRLDOWN}{SHIFTDOWN}j{SHIFTUP}{CTRLUP}", 0)"#
);
}
#[test]
fn send_text_escapes_before_it_reaches_the_dll() {
let h = Harness::new();
h.ai.send_text("Macro{123}!").unwrap();
assert_eq!(h.log(), r#"AU3_Send("Macro{{}123{}}{!}", 0)"#);
}
#[test]
fn send_never_uses_raw_mode() {
let h = Harness::new();
h.ai.send_text("plain").unwrap();
assert!(h.log().ends_with(", 0)"), "{}", h.log());
}
#[test]
fn non_ascii_survives_the_whole_stack() {
let h = Harness::new();
h.ai.send_text("Ünïcödé ãõç — 1.234,56").unwrap();
assert!(h.log().contains("Ünïcödé ãõç — 1.234,56"), "{}", h.log());
}
#[test]
fn selectors_reach_the_dll_in_autoits_own_syntax() {
let h = Harness::new();
let sel = Selector::from("[CLASS:Chrome_WidgetWin_1;TITLE:Acme Invoices]");
h.ai.win_exists(&sel).unwrap();
assert_eq!(
h.log(),
r#"AU3_WinExists("[CLASS:Chrome_WidgetWin_1;TITLE:Acme Invoices]", "")"#
);
}
#[test]
fn a_bare_title_is_sent_as_a_bare_title() {
let h = Harness::new();
h.ai.win_exists(&Selector::title("Order Entry")).unwrap();
assert_eq!(h.log(), r#"AU3_WinExists("Order Entry", "")"#);
}
#[test]
fn the_window_text_parameter_is_always_empty() {
let h = Harness::new();
h.ai.win_activate(&Selector::active()).unwrap();
assert_eq!(h.log(), r#"AU3_WinActivate("[ACTIVE]", "")"#);
}
#[test]
fn a_short_value_comes_back_in_one_call() {
let h = Harness::new();
h.script_string("none");
assert_eq!(h.ai.clip_get().unwrap(), "none");
assert_eq!(h.calls().len(), 1, "should not have needed a retry");
}
#[test]
fn a_value_larger_than_the_buffer_is_retried_until_it_fits() {
let h = Harness::new();
let long = "ã".repeat(3000);
h.script_string(&long);
let got = h.ai.win_get_title(&Selector::active()).unwrap();
assert_eq!(got.chars().count(), 3000, "value was truncated");
assert_eq!(got, long);
assert!(
h.calls().len() > 1,
"expected a retry, got {} call(s)",
h.calls().len()
);
}
#[test]
fn an_empty_clipboard_is_not_an_error() {
let h = Harness::new();
h.script_string("");
assert_eq!(h.ai.clip_get().unwrap(), "");
}
#[test]
fn interior_nul_is_rejected_rather_than_truncating_silently() {
let h = Harness::new();
let err = h.ai.clip_put("before\0after").unwrap_err();
assert!(
matches!(err, autoitx::Error::InteriorNul { at: 6, .. }),
"{err:?}"
);
assert!(h.calls().is_empty(), "must not have called the DLL");
}
#[test]
fn win_wait_activate_checks_before_activating() {
let h = Harness::new();
let sel = Selector::from("[CLASS:Chrome_WidgetWin_1]");
h.ai.win_wait_activate(&sel, Some(Duration::from_secs(30)))
.unwrap();
assert_eq!(
h.call_names(),
["AU3_WinActive", "AU3_WinActivate", "AU3_WinWaitActive"]
);
assert!(
h.calls()[2].ends_with(", 30)"),
"timeout not passed through: {:?}",
h.calls()[2]
);
}
#[test]
fn win_wait_activate_skips_activation_when_already_focused() {
let h = Harness::new();
h.script_int(1);
h.ai.win_wait_activate(&Selector::active(), None).unwrap();
assert_eq!(
h.call_names(),
["AU3_WinActive", "AU3_WinWaitActive"],
"should not have re-activated an already-focused window"
);
}
#[test]
fn no_timeout_is_encoded_as_autoits_wait_forever() {
let h = Harness::new();
h.ai.win_wait_close(&Selector::active(), None).unwrap();
assert!(h.log().ends_with(", 0)"), "{}", h.log());
}
#[test]
fn a_sub_second_timeout_does_not_round_down_to_wait_forever() {
let h = Harness::new();
h.ai.win_wait_close(&Selector::active(), Some(Duration::from_millis(500)))
.unwrap();
assert!(h.log().ends_with(", 1)"), "{}", h.log());
}
#[test]
fn maximize_uses_the_sw_maximize_value() {
let h = Harness::new();
h.ai.maximize(&Selector::from("[CLASS:Chrome_WidgetWin_1]"))
.unwrap();
assert_eq!(
h.log(),
r#"AU3_WinSetState("[CLASS:Chrome_WidgetWin_1]", "", 3)"#
);
assert_eq!(ShowState::Maximize as i32, 3);
}
#[test]
fn close_if_exists_does_nothing_when_no_window_matches() {
let h = Harness::new();
let closed =
h.ai.win_close_if_exists(&Selector::active(), Duration::from_secs(60))
.unwrap();
assert!(!closed);
assert_eq!(
h.call_names(),
["AU3_WinGetHandle"],
"only the identity lookup"
);
}
#[test]
fn close_if_exists_pins_the_window_by_handle_before_escalating() {
let h = Harness::new();
h.script_ints(&[
0x4_0B1E, 1, 0, 4242, 1, 1, ]);
let closed =
h.ai.win_close_if_exists(&Selector::active(), Duration::from_millis(50))
.unwrap();
assert!(closed);
assert_eq!(
h.call_names(),
[
"AU3_WinGetHandle",
"AU3_WinClose",
"AU3_WinWaitClose",
"AU3_WinGetProcess",
"AU3_ProcessClose",
"AU3_WinWaitClose",
],
"escalation order changed"
);
for call in h.calls().iter().skip(1) {
if call.starts_with("AU3_ProcessClose") {
continue;
}
assert!(
call.contains("[HANDLE:40b1e]"),
"call still uses an unstable selector: {call}"
);
}
assert_eq!(
h.calls().iter().filter(|c| c.contains("ACTIVE")).count(),
1,
"[ACTIVE] should be resolved once and never used again:\n{}",
h.log()
);
assert!(h.calls()[0].contains("[ACTIVE]"), "{}", h.calls()[0]);
assert!(
h.calls()[4].contains("4242"),
"killed the wrong process: {:?}",
h.calls()[4]
);
}
#[test]
fn close_if_exists_stops_early_when_the_window_closes_politely() {
let h = Harness::new();
h.script_ints(&[0x4_0B1E, 1, 1]);
let closed =
h.ai.win_close_if_exists(&Selector::active(), Duration::from_secs(1))
.unwrap();
assert!(closed);
assert_eq!(
h.call_names(),
["AU3_WinGetHandle", "AU3_WinClose", "AU3_WinWaitClose"],
"should not have escalated"
);
}
#[test]
fn close_if_exists_reports_a_process_that_will_not_die() {
let h = Harness::new();
h.script_ints(&[0x4_0B1E, 1, 0, 4242, 1, 0]);
let err =
h.ai.win_close_if_exists(&Selector::active(), Duration::from_millis(50))
.unwrap_err();
assert!(
matches!(
err,
autoitx::Error::Timeout {
operation: "win_close_if_exists",
..
}
),
"{err:?}"
);
}
#[test]
fn win_get_handle_reports_a_missing_window() {
let h = Harness::new(); let err = h.ai.win_get_handle(&Selector::active()).unwrap_err();
assert!(
matches!(err, autoitx::Error::WindowNotFound { .. }),
"{err:?}"
);
}
#[test]
fn mouse_click_defaults_to_one_left_click_at_autoits_speed() {
let h = Harness::new();
h.script_int(1); h.ai.mouse_click(Point::new(1350, 290)).unwrap();
assert_eq!(
h.log(),
r#"AU3_MouseClick("left", 1350, 290, 1, INTDEFAULT)"#
);
}
#[test]
fn a_failed_click_is_an_error_rather_than_silence() {
let h = Harness::new();
let err = h.ai.mouse_click(Point::new(-1, -1)).unwrap_err();
assert!(
matches!(
err,
autoitx::Error::AutoItFailed {
func: "AU3_MouseClick",
..
}
),
"{err:?}"
);
}
#[test]
fn click_in_window_anchors_to_the_window_rather_than_the_screen() {
let h = Harness::new();
let sel = Selector::from("[TITLE:Acme ERP;CLASS:ui60Modal_W32]");
h.script_ints(&[1, 1]);
recipes::click_in_window(&h.ai, &sel, 600, 420).unwrap();
let calls = h.calls();
assert_eq!(calls.len(), 2, "{calls:#?}");
assert!(calls[0].starts_with("AU3_WinGetPos("), "{calls:#?}");
assert_eq!(
calls[1],
r#"AU3_MouseClick("left", 600, 420, 1, INTDEFAULT)"#
);
}
#[test]
fn click_in_window_fails_loudly_when_the_window_is_gone() {
let h = Harness::new();
h.script_error(1); let err = recipes::click_in_window(&h.ai, &Selector::active(), 10, 10).unwrap_err();
assert!(
matches!(err, autoitx::Error::WindowNotFound { .. }),
"{err:?}"
);
assert_eq!(h.call_names(), ["AU3_WinGetPos"], "must not have clicked");
}
#[test]
fn win_get_pos_trusts_the_error_flag_not_the_integer_return() {
let h = Harness::new();
let rect =
h.ai.win_get_pos(&Selector::active())
.expect("a clear error flag means the window was found");
assert_eq!(rect, autoitx::Rect::new(0, 0, 0, 0));
h.script_error(1);
assert!(h.ai.win_get_pos(&Selector::active()).is_err());
}
#[test]
fn win_get_process_rejects_the_minus_one_sentinel() {
let h = Harness::new();
h.script_int(-1);
let err = h.ai.win_get_process(&Selector::active()).unwrap_err();
assert!(
matches!(err, autoitx::Error::WindowNotFound { .. }),
"expected WindowNotFound, got {err:?}"
);
}
#[test]
fn close_if_exists_never_tries_to_kill_the_sentinel_pid() {
let h = Harness::new();
h.script_ints(&[
1, 1, 0, -1, ]);
let err =
h.ai.win_close_if_exists(&Selector::active(), Duration::from_millis(50))
.unwrap_err();
assert!(
matches!(err, autoitx::Error::WindowNotFound { .. }),
"{err:?}"
);
assert!(
!h.call_names().contains(&"AU3_ProcessClose".to_owned()),
"must not have attempted a kill: {:#?}",
h.call_names()
);
}
#[test]
fn win_get_title_cannot_distinguish_missing_from_untitled() {
let h = Harness::new();
h.script_string("");
assert_eq!(h.ai.win_get_title(&Selector::active()).unwrap(), "");
h.script_string("");
h.script_error(0); assert_eq!(h.ai.win_get_title(&Selector::active()).unwrap(), "");
}
#[test]
fn win_get_class_list_does_report_a_missing_window() {
let h = Harness::new();
h.script_string("");
let err = h.ai.win_get_class_list(&Selector::active()).unwrap_err();
assert!(
matches!(err, autoitx::Error::WindowNotFound { .. }),
"{err:?}"
);
}
#[test]
fn win_get_class_list_splits_on_newlines() {
let h = Harness::new();
h.script_string("Static\nEdit\nButton");
assert_eq!(
h.ai.win_get_class_list(&Selector::active()).unwrap(),
["Static", "Edit", "Button"]
);
}
#[test]
fn process_exists_actually_returns_a_process_id() {
let h = Harness::new();
h.script_int(4720);
assert_eq!(h.ai.process_id("notepad.exe").unwrap(), Some(4720));
assert!(h.ai.process_exists("notepad.exe").unwrap());
h.script_int(0);
assert_eq!(h.ai.process_id("__nao_existe__.exe").unwrap(), None);
assert!(!h.ai.process_exists("__nao_existe__.exe").unwrap());
}
#[test]
fn win_get_state_reports_through_the_error_flag() {
let h = Harness::new();
h.script_int(15);
let state = h.ai.win_get_state(&Selector::active()).unwrap();
assert!(state.contains(autoitx::WinState::EXISTS));
assert!(state.contains(autoitx::WinState::ACTIVE));
assert!(!state.contains(autoitx::WinState::MINIMIZED));
h.script_error(1);
assert!(h.ai.win_get_state(&Selector::active()).is_err());
}
#[test]
fn activate_and_set_state_report_whether_the_window_was_found() {
let h = Harness::new();
h.script_ints(&[0, 1, 0, 1]);
assert!(!h.ai.win_activate(&Selector::active()).unwrap());
assert!(h.ai.win_activate(&Selector::active()).unwrap());
assert!(!h.ai.maximize(&Selector::active()).unwrap());
assert!(h.ai.maximize(&Selector::active()).unwrap());
}
#[test]
fn read_screen_text_selects_copies_then_reads() {
let h = Harness::new();
h.script_string("115597/1");
let resultado = recipes::read_screen_text(
&h.ai,
keys!("{END}{SHIFTDOWN}{HOME}{SHIFTUP}"),
Duration::from_millis(300),
);
let calls = h.call_names();
assert_eq!(
&calls[..2],
["AU3_Send", "AU3_Send"],
"select then copy, before any read: {calls:#?}"
);
assert_eq!(
h.calls()[0],
r#"AU3_Send("{END}{SHIFTDOWN}{HOME}{SHIFTUP}", 0)"#
);
assert_eq!(h.calls()[1], r#"AU3_Send("{CTRLDOWN}c{CTRLUP}", 0)"#);
match h.ai.clip_sequence() {
None => {
assert_eq!(resultado.unwrap(), "115597/1");
assert_eq!(calls[2], "AU3_ClipGet", "must read only after copying");
}
Some(_) => {
let err = resultado.expect_err(
"with a real sequence counter and a mock clipboard, the counter \
cannot move — this must time out rather than return stale text",
);
assert!(
matches!(
err,
autoitx::Error::Timeout {
operation: "read_screen_text",
..
}
),
"{err:?}"
);
}
}
}
#[test]
fn read_screen_text_never_writes_to_the_clipboard() {
let h = Harness::new();
h.script_string("qualquer coisa");
let _ = recipes::read_screen_text(&h.ai, keys!("{END}"), Duration::from_millis(300));
assert!(
!h.call_names().contains(&"AU3_ClipPut".to_owned()),
"must not have written to the clipboard: {:#?}",
h.call_names()
);
}
#[test]
fn wait_until_idle_gives_up_instead_of_hanging_forever() {
let h = Harness::new();
h.script_int(15);
let err = recipes::wait_until_idle(&h.ai, Duration::from_millis(300)).unwrap_err();
assert!(
matches!(
err,
autoitx::Error::Timeout {
operation: "wait_until_idle",
..
}
),
"{err:?}"
);
}
#[test]
fn wait_until_idle_accepts_the_ibeam_cursor_too() {
let h = Harness::new();
h.script_int(5); recipes::wait_until_idle(&h.ai, Duration::from_secs(1)).unwrap();
}
#[test]
fn a_session_can_make_nested_calls_without_deadlocking() {
let h = Harness::new();
let s = h.ai.session();
s.send(keys!("{TAB}")).unwrap();
s.clip_put("x").unwrap();
s.win_exists(&Selector::active()).unwrap();
drop(s);
assert_eq!(h.calls().len(), 3);
}
#[test]
fn keys_and_text_compose() {
let h = Harness::new();
let seq = Keys::text("74").then(keys!("{TAB}"));
h.ai.send(seq).unwrap();
assert_eq!(h.log(), r#"AU3_Send("74{TAB}", 0)"#);
}
#[test]
fn a_long_title_needing_growth_still_round_trips_non_ascii() {
let h = Harness::new();
let long = "Ünïcödé ãõç — ".repeat(300);
h.script_string(&long);
assert_eq!(h.ai.win_get_title(&Selector::active()).unwrap(), long);
}
#[test]
fn wait_for_any_returns_the_watch_that_fired() {
let h = Harness::new();
h.script_ints(&[0, 0, 1]);
let a = Selector::from("A");
let b = Selector::from("B");
let c = Selector::from("C");
let fired =
h.ai.wait_for_any(
&[
(&a, WinCondition::Exists),
(&b, WinCondition::Exists),
(&c, WinCondition::Exists),
],
Some(Duration::from_secs(1)),
)
.unwrap();
assert_eq!(fired, Some(2));
assert_eq!(
h.call_names(),
["AU3_WinExists", "AU3_WinExists", "AU3_WinExists"],
"every watch should be evaluated within the pass"
);
}
#[test]
fn wait_for_any_prefers_the_lower_index_on_a_tie() {
let h = Harness::new();
h.script_ints(&[0, 1, 1]);
let a = Selector::from("A");
let b = Selector::from("B");
let c = Selector::from("C");
let fired =
h.ai.wait_for_any(
&[
(&a, WinCondition::Exists),
(&b, WinCondition::Exists),
(&c, WinCondition::Exists),
],
Some(Duration::from_secs(1)),
)
.unwrap();
assert_eq!(fired, Some(1));
assert_eq!(
h.call_names().len(),
2,
"should stop at the first match, not finish the pass"
);
}
#[test]
fn wait_for_any_reports_giving_up_as_none() {
let h = Harness::new();
h.script_int(0);
let a = Selector::from("A");
let started = std::time::Instant::now();
let fired =
h.ai.wait_for_any(
&[(&a, WinCondition::Exists)],
Some(Duration::from_millis(10)),
)
.unwrap();
assert_eq!(fired, None);
assert!(
started.elapsed() >= Duration::from_millis(10),
"should have waited out the timeout, not returned early"
);
}
#[test]
fn wait_for_any_without_watches_returns_instead_of_hanging() {
let h = Harness::new();
assert_eq!(h.ai.wait_for_any(&[], None).unwrap(), None);
assert!(h.call_names().is_empty(), "nothing to ask about");
}
#[test]
fn wait_for_any_inverts_exists_for_gone() {
let h = Harness::new();
h.script_ints(&[0]);
let a = Selector::from("A");
let fired =
h.ai.wait_for_any(&[(&a, WinCondition::Gone)], Some(Duration::from_secs(1)))
.unwrap();
assert_eq!(fired, Some(0));
assert_eq!(h.call_names(), ["AU3_WinExists"]);
}
#[test]
fn wait_for_any_asks_about_focus_for_active() {
let h = Harness::new();
h.script_ints(&[1]);
let a = Selector::from("A");
let fired =
h.ai.wait_for_any(&[(&a, WinCondition::Active)], Some(Duration::from_secs(1)))
.unwrap();
assert_eq!(fired, Some(0));
assert_eq!(h.call_names(), ["AU3_WinActive"]);
}