cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use std::fs;
use std::thread;
use std::time::{Duration, Instant};

use super::tests::Fixture;
use super::{AccessibilitySurface, WindowInspectResult, inspect_window_surface, press, set_value};

fn menu(fixture: &Fixture, root: &str) -> WindowInspectResult {
    inspect_window_surface(
        fixture.window_id,
        AccessibilitySurface::Menu,
        root,
        200,
        8,
        false,
        false,
    )
    .expect("menu inspection")
}

fn target(result: &WindowInspectResult, name: &str) -> String {
    result
        .accessibility
        .nodes
        .iter()
        .find(|n| n.name == name)
        .and_then(|n| n.target.clone())
        .unwrap_or_else(|| panic!("missing target: {name}"))
}

fn wait_state(fixture: &Fixture, predicate: impl Fn(&serde_json::Value) -> bool) {
    let deadline = Instant::now() + Duration::from_secs(3);
    loop {
        let state: serde_json::Value = serde_json::from_slice(
            &fs::read(fixture.directory.path().join("state.json")).expect("state"),
        )
        .expect("JSON");
        assert_eq!(state["active"], false);
        if predicate(&state) {
            return;
        }
        assert!(Instant::now() < deadline, "unexpected state: {state}");
        thread::sleep(Duration::from_millis(30));
    }
}

#[test]
#[ignore = "requires a logged-in macOS desktop and Accessibility permission"]
fn menu_exploration_executes_leaf_actions_and_discovers_standard_sheets() {
    let fixture =
        Fixture::launch_source(include_str!("menu_fixture.swift"), "Cueward Menu Fixture");
    let overview = menu(&fixture, "0");
    assert_eq!(overview.accessibility.surface, AccessibilitySurface::Menu);
    assert!(
        overview
            .accessibility
            .nodes
            .iter()
            .filter(|n| n.role != "AXMenuItem" || n.child_count > 0)
            .all(|n| n.target.is_none())
    );
    let item = overview
        .accessibility
        .nodes
        .iter()
        .find(|n| n.name == "Increment")
        .expect("item");
    let detail = menu(&fixture, &item.r#ref);
    press(&target(&detail, "Increment")).expect("background menu action");
    wait_state(&fixture, |s| s["count"] == 1);
    press(&target(&menu(&fixture, "0"), "Save Document")).expect("open sheet");
    wait_state(&fixture, |s| s["sheet"].as_u64().is_some_and(|id| id > 0));
    let observed = fixture.inspect();
    assert!(
        observed
            .accessibility
            .nodes
            .iter()
            .any(|n| n.role == "AXSheet")
    );
    for node in &observed.accessibility.nodes {
        if node.enabled == Some(false) {
            assert!(node.target.is_none());
        }
    }
    let name = observed
        .accessibility
        .nodes
        .iter()
        .find(|n| n.value.as_deref() == Some("Result.txt"))
        .and_then(|n| n.target.as_deref())
        .expect("filename field");
    let changed = set_value(name, "Edited.txt").expect("edit filename");
    assert_eq!(changed.status, super::ActionStatus::Confirmed);
    press(&target(&fixture.inspect(), "Cancel")).expect("cancel sheet");
    wait_state(&fixture, |s| s["result"] == "cancelled" && s["sheet"] == 0);
    assert!(!fixture.directory.path().join("Edited.txt").exists());
}