use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UIElement {
pub text: String,
pub center: [i32; 2],
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub class: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
#[serde(skip_serializing_if = "is_true")]
pub enabled: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub checked: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub focused: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub editable: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub scrollable: bool,
#[serde(skip)]
pub score: i32,
}
fn is_true(v: &bool) -> bool {
*v
}
impl UIElement {
pub fn hash_key(&self) -> String {
format!(
"{}|{}|{},{}|{}|{}",
self.id.as_deref().unwrap_or(""),
self.text,
self.center[0],
self.center[1],
self.enabled,
self.checked,
)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ScreenState {
pub package: String,
pub screen_size: [i32; 2],
pub elements: Vec<UIElement>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", content = "message")]
pub enum StuckAlert {
ScreenUnchanged(String),
ActionRepeated(String),
NavigationDrift(String),
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_element() -> UIElement {
UIElement {
text: "OK".into(),
center: [540, 1200],
action: "tap".into(),
class: Some("Button".into()),
id: Some("com.app:id/btn_ok".into()),
hint: None,
enabled: true,
checked: false,
focused: false,
editable: false,
scrollable: false,
score: 15,
}
}
#[test]
fn test_ui_element_hash_key() {
let elem = sample_element();
let hash = elem.hash_key();
assert!(hash.contains("com.app:id/btn_ok"));
assert!(hash.contains("OK"));
assert!(hash.contains("540,1200"));
}
#[test]
fn test_ui_element_serialize_compact() {
let elem = sample_element();
let json = serde_json::to_string(&elem).unwrap();
assert!(!json.contains("\"enabled\""));
assert!(!json.contains("\"checked\""));
assert!(!json.contains("\"focused\""));
assert!(!json.contains("\"score\""));
assert!(json.contains("\"text\""));
assert!(json.contains("\"center\""));
}
#[test]
fn test_ui_element_serialize_disabled() {
let mut elem = sample_element();
elem.enabled = false;
let json = serde_json::to_string(&elem).unwrap();
assert!(json.contains("\"enabled\":false"));
}
#[test]
fn test_ui_element_serialize_checked() {
let mut elem = sample_element();
elem.checked = true;
let json = serde_json::to_string(&elem).unwrap();
assert!(json.contains("\"checked\":true"));
}
#[test]
fn test_ui_element_serialize_editable() {
let mut elem = sample_element();
elem.editable = true;
let json = serde_json::to_string(&elem).unwrap();
assert!(json.contains("\"editable\":true"));
}
#[test]
fn test_screen_state_serialize() {
let state = ScreenState {
package: "com.example.app".into(),
screen_size: [1080, 2400],
elements: vec![sample_element()],
};
let json = serde_json::to_string(&state).unwrap();
assert!(json.contains("\"package\":\"com.example.app\""));
assert!(json.contains("\"screen_size\":[1080,2400]"));
}
#[test]
fn test_stuck_alert_serialize() {
let alert = StuckAlert::ScreenUnchanged("Screen unchanged for 3 steps".into());
let json = serde_json::to_string(&alert).unwrap();
assert!(json.contains("\"type\":\"ScreenUnchanged\""));
}
#[test]
fn test_ui_element_no_id() {
let mut elem = sample_element();
elem.id = None;
let hash = elem.hash_key();
assert!(hash.starts_with('|'));
}
#[test]
fn test_ui_element_hint_omitted_when_none() {
let elem = sample_element();
let json = serde_json::to_string(&elem).unwrap();
assert!(!json.contains("\"hint\""));
}
#[test]
fn test_ui_element_hint_present_when_some() {
let mut elem = sample_element();
elem.hint = Some("Enter name".into());
let json = serde_json::to_string(&elem).unwrap();
assert!(json.contains("\"hint\":\"Enter name\""));
}
}