mod common;
use common::*;
#[test]
fn on_failure_continue_proceeds_to_next_step() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: this step will time out
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=DoesNotExist]"
timeout: 50ms
on_failure: continue
- intent: this step must still run
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"],
);
}
#[test]
fn on_failure_continue_multiple_steps_all_continue() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: step 1 times out
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=Missing1]"
timeout: 50ms
on_failure: continue
- intent: step 2 times out
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=Missing2]"
timeout: 50ms
on_failure: continue
- intent: step 3 succeeds
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"],
);
}
#[test]
fn on_failure_abort_is_default() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: this step times out with default on_failure
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=DoesNotExist]"
timeout: 50ms
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_err(), "default abort must propagate error");
assert_eq!(
event_names(&events),
["started:main", "failed:main", "Failed"],
);
}
#[test]
fn on_success_return_phase_stops_remaining_steps() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: early return
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
on_success: return_phase
- intent: this must NOT run
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=DoesNotExist]"
timeout: 50ms
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"],
);
}
#[test]
fn on_success_return_phase_subsequent_phases_run() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: phase_a
mount: [app]
steps:
- intent: return early from phase_a
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
on_success: return_phase
- name: phase_b
steps: []
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
[
"started:phase_a",
"completed:phase_a",
"started:phase_b",
"completed:phase_b",
"Completed",
],
);
}
#[test]
fn on_failure_fallback_runs_when_primary_times_out() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: primary times out, fallback satisfies expect
action:
type: NoOp
fallback:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
timeout: 50ms
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"],
);
}
#[test]
fn on_failure_fallback_fails_when_fallback_expect_unsatisfied() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: both primary and fallback timeout — phase fails
action:
type: NoOp
fallback:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=NeverExists]"
timeout: 50ms
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_err(), "fallback that cannot fix expect must fail");
assert_eq!(
event_names(&events),
["started:main", "failed:main", "Failed"],
);
}
#[test]
fn on_failure_fallback_parses_correctly() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: fallback with explicit scope
action:
type: NoOp
fallback:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
timeout: 50ms
"#;
parse(yaml);
}
#[test]
fn on_failure_continue_then_on_success_return_phase() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: step 1 fails, continue
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=Missing]"
timeout: 50ms
on_failure: continue
- intent: step 2 succeeds, return_phase
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
on_success: return_phase
- intent: step 3 must not run
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=AlsoMissing]"
timeout: 50ms
"#;
let desktop = ui_automata::mock::mock_desktop_from_yaml(
r#"
role: window
name: App
"#,
);
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"],
);
}
#[test]
fn action_error_with_always_aborts_step() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: action fails, Always must not hide it
action:
type: Click
scope: app
selector: ">> [role=button][name=DoesNotExist]"
expect:
type: Always
timeout: 50ms
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(
result.is_err(),
"action error must propagate even with Always"
);
assert_eq!(
event_names(&events),
["started:main", "failed:main", "Failed"]
);
}
#[test]
fn action_error_always_on_failure_continue_ok() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: action fails but on_failure=continue allows it
action:
type: Click
scope: app
selector: ">> [role=button][name=DoesNotExist]"
expect:
type: Always
on_failure: continue
- intent: must still run
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"]
);
}
#[test]
fn action_error_with_always_retries_then_fails() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: action always fails, retry exhausted
action:
type: Click
scope: app
selector: ">> [role=button][name=DoesNotExist]"
expect:
type: Always
timeout: 50ms
retry:
fixed:
count: 2
delay: 0s
"#;
let desktop = app_desktop();
let (result, _) = run(yaml, desktop);
assert!(result.is_err(), "exhausted retries must still fail");
let msg = result.unwrap_err().to_string();
assert!(msg.contains("timed out after 3 attempt"), "got: {msg}");
}
#[test]
fn action_error_on_retry_1_then_ok_on_retry_2_succeeds() {
use ui_automata::mock::MockElement;
let trigger = MockElement::leaf("button", "Trigger");
trigger.kill();
let desktop = ui_automata::mock::MockDesktop::new(vec![MockElement::parent(
"window",
"App",
vec![trigger.clone()],
)]);
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: element dead on attempt 1, alive on attempt 2
action:
type: Click
scope: app
selector: ">> [role=button][name=Trigger]"
expect:
type: Always
timeout: 50ms
retry:
fixed:
count: 1
delay: 200ms
"#;
let t = trigger.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(50));
t.revive();
});
let (result, events) = run(yaml, desktop);
assert!(
result.is_ok(),
"attempt 2 succeeds → step must succeed, not carry stale error: {result:?}"
);
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"]
);
}
#[test]
fn step_precondition_satisfied_step_runs() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: precondition true, step must run
precondition:
type: ElementFound
scope: app
selector: "[name=App]"
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"]
);
}
#[test]
fn step_precondition_not_satisfied_step_skipped() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: precondition false → skip; would fail if it ran
precondition:
type: ElementFound
scope: app
selector: ">> [role=button][name=NeverExists]"
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=AlsoNeverExists]"
timeout: 50ms
- intent: must still run after skip
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(result.is_ok(), "{result:?}");
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"]
);
}
#[test]
fn timeout_retry_exhausted_fails_with_attempt_count() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: action ok but expect never satisfied
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=NeverExists]"
timeout: 50ms
retry:
fixed:
count: 2
delay: 0s
"#;
let desktop = app_desktop();
let (result, _) = run(yaml, desktop);
assert!(result.is_err(), "must fail when expect never satisfied");
let msg = result.unwrap_err().to_string();
assert!(msg.contains("timed out after 3 attempt"), "got: {msg}");
}
#[test]
fn recovery_skip_step_skips_and_continues() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
recovery_handlers:
skip_it:
trigger:
type: ElementFound
scope: app
selector: "[name=App]"
actions: []
resume: skip_step
phases:
- name: main
mount: [app]
recovery:
handlers: [skip_it]
steps:
- intent: will time out, recovery skips it
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=NeverExists]"
timeout: 50ms
- intent: must run after skip
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(
result.is_ok(),
"skip_step recovery must allow phase to complete: {result:?}"
);
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"]
);
}
#[test]
fn recovery_fail_resume_aborts_step() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
recovery_handlers:
fail_it:
trigger:
type: ElementFound
scope: app
selector: "[name=App]"
actions: []
resume: fail
phases:
- name: main
mount: [app]
recovery:
handlers: [fail_it]
steps:
- intent: will time out, recovery fails it
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=NeverExists]"
timeout: 50ms
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(result.is_err(), "fail resume must abort the step");
assert_eq!(
event_names(&events),
["started:main", "failed:main", "Failed"]
);
}
#[test]
fn recovery_retry_step_reruns_step_and_succeeds() {
use ui_automata::mock::MockElement;
let btn = MockElement::leaf("button", "Target");
btn.kill();
let desktop = ui_automata::mock::MockDesktop::new(vec![MockElement::parent(
"window",
"App",
vec![btn.clone()],
)]);
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
recovery_handlers:
revive_handler:
trigger:
type: ElementFound
scope: app
selector: "[name=App]"
actions: []
resume: retry_step
phases:
- name: main
mount: [app]
recovery:
handlers: [revive_handler]
steps:
- intent: target missing on attempt 1; recovery retries; target present on retry
action:
type: Click
scope: app
selector: ">> [role=button][name=Target]"
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=Target]"
timeout: 100ms
"#;
let b = btn.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(40));
b.revive();
});
let (result, events) = run(yaml, desktop);
assert!(
result.is_ok(),
"step must succeed after recovery retry: {result:?}"
);
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"]
);
}
#[test]
fn recovery_max_recoveries_reached_then_fails() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
recovery_handlers:
always_retry:
trigger:
type: ElementFound
scope: app
selector: "[name=App]"
actions: []
resume: retry_step
phases:
- name: main
mount: [app]
recovery:
handlers: [always_retry]
limit: 2
steps:
- intent: always times out, recovery retries until limit
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=NeverExists]"
timeout: 50ms
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(
result.is_err(),
"must fail once max_recoveries is exhausted"
);
assert_eq!(
event_names(&events),
["started:main", "failed:main", "Failed"]
);
}
#[test]
fn fallback_success_with_return_phase_skips_remaining() {
let yaml = r#"
name: test
anchors:
app:
type: Root
selector: "[name=App]"
phases:
- name: main
mount: [app]
steps:
- intent: primary times out; fallback satisfies; return_phase skips step 2
action:
type: NoOp
fallback:
type: NoOp
expect:
type: ElementFound
scope: app
selector: "[name=App]"
on_success: return_phase
timeout: 50ms
- intent: must NOT run — would fail if reached
action:
type: NoOp
expect:
type: ElementFound
scope: app
selector: ">> [role=button][name=NeverExists]"
timeout: 50ms
"#;
let desktop = app_desktop();
let (result, events) = run(yaml, desktop);
assert!(
result.is_ok(),
"fallback + return_phase must complete the phase: {result:?}"
);
assert_eq!(
event_names(&events),
["started:main", "completed:main", "Completed"]
);
}