use crate::element::ScreenContext;
use crate::events::CelEvent;
use std::collections::HashSet;
pub struct ContextWatchdog {
last_element_ids: HashSet<String>,
last_focused_id: Option<String>,
last_network_idle: bool,
initialized: bool,
}
impl Default for ContextWatchdog {
fn default() -> Self {
Self::new()
}
}
impl ContextWatchdog {
pub fn new() -> Self {
Self {
last_element_ids: HashSet::new(),
last_focused_id: None,
last_network_idle: true,
initialized: false,
}
}
pub fn tick(&mut self, context: &ScreenContext, network_idle: bool) -> Vec<CelEvent> {
let current_ids: HashSet<String> = context.elements.iter().map(|e| e.id.clone()).collect();
let current_focused = context
.elements
.iter()
.find(|e| e.state.focused)
.map(|e| e.id.clone());
if !self.initialized {
self.last_element_ids = current_ids;
self.last_focused_id = current_focused;
self.last_network_idle = network_idle;
self.initialized = true;
return vec![];
}
let mut events = Vec::new();
let added: Vec<String> = current_ids
.difference(&self.last_element_ids)
.cloned()
.collect();
let removed: Vec<String> = self
.last_element_ids
.difference(¤t_ids)
.cloned()
.collect();
if !added.is_empty() || !removed.is_empty() {
events.push(CelEvent::TreeChanged { added, removed });
}
if current_focused != self.last_focused_id {
events.push(CelEvent::FocusChanged {
old: self.last_focused_id.clone(),
new: current_focused.clone(),
});
}
if network_idle && !self.last_network_idle {
events.push(CelEvent::NetworkIdle);
}
self.last_element_ids = current_ids;
self.last_focused_id = current_focused;
self.last_network_idle = network_idle;
events
}
pub fn merge_events(&self, events: Vec<CelEvent>) -> Vec<CelEvent> {
events
}
pub fn merge_ax_events<T>(&self, _events: Vec<T>) -> Vec<CelEvent> {
Vec::new()
}
pub fn reset(&mut self) {
self.last_element_ids.clear();
self.last_focused_id = None;
self.last_network_idle = true;
self.initialized = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::element::{Bounds, ContentRole, ContextElement, ContextSource, ElementState};
fn make_element(id: &str, focused: bool) -> ContextElement {
ContextElement {
id: id.to_string(),
label: Some(format!("Element {}", id)),
description: None,
element_type: "button".to_string(),
value: None,
bounds: Some(Bounds {
x: 0,
y: 0,
width: 100,
height: 30,
}),
state: ElementState {
focused,
enabled: true,
visible: true,
selected: false,
expanded: None,
checked: None,
},
parent_id: None,
actions: vec![],
properties: std::collections::HashMap::new(),
confidence: 0.85,
source: ContextSource::AccessibilityTree,
content_role: ContentRole::default(),
}
}
fn make_context(elements: Vec<ContextElement>) -> ScreenContext {
ScreenContext {
app: "Test".into(),
window: "Test Window".into(),
elements,
network_events: vec![],
http_events: vec![],
timestamp_ms: 0,
screen_width: None,
screen_height: None,
clipboard: None,
window_list: vec![],
audio: None,
power: None,
running_apps: vec![],
recent_files: vec![],
transcripts: vec![],
}
}
#[test]
fn test_first_tick_no_events() {
let mut wd = ContextWatchdog::new();
let ctx = make_context(vec![make_element("a", true)]);
let events = wd.tick(&ctx, true);
assert!(events.is_empty(), "First tick should not emit events");
}
#[test]
fn test_no_change_no_events() {
let mut wd = ContextWatchdog::new();
let ctx = make_context(vec![make_element("a", true)]);
wd.tick(&ctx, true); let events = wd.tick(&ctx, true);
assert!(events.is_empty());
}
#[test]
fn test_element_added() {
let mut wd = ContextWatchdog::new();
let ctx1 = make_context(vec![make_element("a", true)]);
wd.tick(&ctx1, true);
let ctx2 = make_context(vec![make_element("a", true), make_element("b", false)]);
let events = wd.tick(&ctx2, true);
assert_eq!(events.len(), 1);
match &events[0] {
CelEvent::TreeChanged { added, removed } => {
assert_eq!(added, &["b"]);
assert!(removed.is_empty());
}
_ => panic!("Expected TreeChanged"),
}
}
#[test]
fn test_element_removed() {
let mut wd = ContextWatchdog::new();
let ctx1 = make_context(vec![make_element("a", true), make_element("b", false)]);
wd.tick(&ctx1, true);
let ctx2 = make_context(vec![make_element("a", true)]);
let events = wd.tick(&ctx2, true);
assert_eq!(events.len(), 1);
match &events[0] {
CelEvent::TreeChanged { added, removed } => {
assert!(added.is_empty());
assert_eq!(removed, &["b"]);
}
_ => panic!("Expected TreeChanged"),
}
}
#[test]
fn test_focus_changed() {
let mut wd = ContextWatchdog::new();
let ctx1 = make_context(vec![make_element("a", true), make_element("b", false)]);
wd.tick(&ctx1, true);
let ctx2 = make_context(vec![make_element("a", false), make_element("b", true)]);
let events = wd.tick(&ctx2, true);
assert!(events
.iter()
.any(|e| matches!(e, CelEvent::FocusChanged { .. })));
}
#[test]
fn test_network_idle_transition() {
let mut wd = ContextWatchdog::new();
let ctx = make_context(vec![make_element("a", true)]);
wd.tick(&ctx, false);
let events = wd.tick(&ctx, true); assert!(events.iter().any(|e| matches!(e, CelEvent::NetworkIdle)));
}
#[test]
fn test_network_stays_idle_no_event() {
let mut wd = ContextWatchdog::new();
let ctx = make_context(vec![make_element("a", true)]);
wd.tick(&ctx, true); let events = wd.tick(&ctx, true); assert!(!events.iter().any(|e| matches!(e, CelEvent::NetworkIdle)));
}
#[test]
fn test_reset() {
let mut wd = ContextWatchdog::new();
let ctx = make_context(vec![make_element("a", true)]);
wd.tick(&ctx, true);
wd.reset();
let events = wd.tick(&ctx, true);
assert!(events.is_empty());
}
}