agent_tui/ipc/
snapshot_dto.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5/// Returns true if the boolean is false (used by serde skip_serializing_if).
6#[inline]
7fn is_false(b: &bool) -> bool {
8    !*b
9}
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
12pub struct BoundsDto {
13    pub x: u16,
14    pub y: u16,
15    pub width: u16,
16    pub height: u16,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ElementRefDto {
21    pub role: String,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub name: Option<String>,
24    pub bounds: BoundsDto,
25    pub visual_hash: u64,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub nth: Option<usize>,
28    #[serde(default, skip_serializing_if = "is_false")]
29    pub selected: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RefMapDto {
34    #[serde(flatten)]
35    pub refs: HashMap<String, ElementRefDto>,
36}
37
38#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
39pub struct SnapshotStatsDto {
40    pub total: usize,
41    pub interactive: usize,
42    pub lines: usize,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AccessibilitySnapshotDto {
47    pub tree: String,
48    pub refs: RefMapDto,
49    pub stats: SnapshotStatsDto,
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn test_bounds_dto_independent_creation() {
58        let bounds = BoundsDto {
59            x: 10,
60            y: 5,
61            width: 20,
62            height: 3,
63        };
64        assert_eq!(bounds.x, 10);
65        assert_eq!(bounds.y, 5);
66        assert_eq!(bounds.width, 20);
67        assert_eq!(bounds.height, 3);
68    }
69
70    #[test]
71    fn test_element_ref_dto_independent_creation() {
72        let element = ElementRefDto {
73            role: "button".to_string(),
74            name: Some("OK".to_string()),
75            bounds: BoundsDto {
76                x: 5,
77                y: 10,
78                width: 4,
79                height: 1,
80            },
81            visual_hash: 12345,
82            nth: Some(2),
83            selected: false,
84        };
85        assert_eq!(element.role, "button");
86        assert_eq!(element.name, Some("OK".to_string()));
87        assert_eq!(element.bounds.x, 5);
88        assert_eq!(element.visual_hash, 12345);
89        assert_eq!(element.nth, Some(2));
90    }
91
92    #[test]
93    fn test_snapshot_dto_serialization() {
94        let snapshot = AccessibilitySnapshotDto {
95            tree: "- button \"OK\" [ref=e1]".to_string(),
96            refs: RefMapDto {
97                refs: {
98                    let mut map = HashMap::new();
99                    map.insert(
100                        "e1".to_string(),
101                        ElementRefDto {
102                            role: "button".to_string(),
103                            name: Some("OK".to_string()),
104                            bounds: BoundsDto {
105                                x: 10,
106                                y: 5,
107                                width: 2,
108                                height: 1,
109                            },
110                            visual_hash: 12345,
111                            nth: None,
112                            selected: false,
113                        },
114                    );
115                    map
116                },
117            },
118            stats: SnapshotStatsDto {
119                total: 1,
120                interactive: 1,
121                lines: 1,
122            },
123        };
124
125        let json = serde_json::to_string(&snapshot).unwrap();
126
127        assert!(json.contains("\"tree\""));
128        assert!(json.contains("\"stats\""));
129        assert!(json.contains("\"total\""));
130        assert!(json.contains("\"interactive\""));
131        assert!(json.contains("button"));
132    }
133
134    #[test]
135    fn test_snapshot_dto_roundtrip() {
136        let snapshot = AccessibilitySnapshotDto {
137            tree: "- button \"OK\" [ref=e1]\n- input \">\" [ref=e2]".to_string(),
138            refs: RefMapDto {
139                refs: {
140                    let mut map = HashMap::new();
141                    map.insert(
142                        "e1".to_string(),
143                        ElementRefDto {
144                            role: "button".to_string(),
145                            name: Some("OK".to_string()),
146                            bounds: BoundsDto {
147                                x: 10,
148                                y: 5,
149                                width: 2,
150                                height: 1,
151                            },
152                            visual_hash: 12345,
153                            nth: None,
154                            selected: false,
155                        },
156                    );
157                    map.insert(
158                        "e2".to_string(),
159                        ElementRefDto {
160                            role: "input".to_string(),
161                            name: Some(">".to_string()),
162                            bounds: BoundsDto {
163                                x: 0,
164                                y: 0,
165                                width: 1,
166                                height: 1,
167                            },
168                            visual_hash: 67890,
169                            nth: None,
170                            selected: false,
171                        },
172                    );
173                    map
174                },
175            },
176            stats: SnapshotStatsDto {
177                total: 2,
178                interactive: 2,
179                lines: 2,
180            },
181        };
182
183        let json = serde_json::to_string(&snapshot).unwrap();
184        let restored: AccessibilitySnapshotDto = serde_json::from_str(&json).unwrap();
185
186        assert_eq!(restored.tree, snapshot.tree);
187        assert_eq!(restored.stats.total, snapshot.stats.total);
188        assert_eq!(restored.stats.interactive, snapshot.stats.interactive);
189    }
190
191    #[test]
192    fn test_refs_structure() {
193        let snapshot = AccessibilitySnapshotDto {
194            tree: "- button \"Submit\" [ref=e1]".to_string(),
195            refs: RefMapDto {
196                refs: {
197                    let mut map = HashMap::new();
198                    map.insert(
199                        "e1".to_string(),
200                        ElementRefDto {
201                            role: "button".to_string(),
202                            name: Some("Submit".to_string()),
203                            bounds: BoundsDto {
204                                x: 0,
205                                y: 0,
206                                width: 6,
207                                height: 1,
208                            },
209                            visual_hash: 12345,
210                            nth: None,
211                            selected: false,
212                        },
213                    );
214                    map
215                },
216            },
217            stats: SnapshotStatsDto {
218                total: 1,
219                interactive: 1,
220                lines: 1,
221            },
222        };
223
224        let json = serde_json::to_string_pretty(&snapshot).unwrap();
225
226        assert!(json.contains("\"e1\""));
227        assert!(json.contains("\"role\": \"button\""));
228        assert!(json.contains("\"name\": \"Submit\""));
229        assert!(json.contains("\"bounds\""));
230    }
231}