1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct A11yNode {
14 pub node_id: String,
16 pub role: String,
18 pub name: Option<String>,
20 pub value: Option<String>,
22 pub bounds: Bounds,
24 pub children: Vec<String>,
26 pub focusable: bool,
28 pub focused: bool,
30 pub disabled: bool,
32}
33
34#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
36pub struct Bounds {
37 pub x: f64,
38 pub y: f64,
39 pub width: f64,
40 pub height: f64,
41}
42
43impl Bounds {
44 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
45 Self {
46 x,
47 y,
48 width,
49 height,
50 }
51 }
52
53 pub fn center(&self) -> (f64, f64) {
55 (self.x + self.width / 2.0, self.y + self.height / 2.0)
56 }
57
58 pub fn contains(&self, x: f64, y: f64) -> bool {
60 x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
61 }
62
63 pub fn overlaps(&self, other: &Bounds) -> bool {
65 self.x < other.x + other.width
66 && self.x + self.width > other.x
67 && self.y < other.y + other.height
68 && self.y + self.height > other.y
69 }
70
71 pub fn iou(&self, other: &Bounds) -> f64 {
73 let x1 = self.x.max(other.x);
74 let y1 = self.y.max(other.y);
75 let x2 = (self.x + self.width).min(other.x + other.width);
76 let y2 = (self.y + self.height).min(other.y + other.height);
77
78 if x2 <= x1 || y2 <= y1 {
79 return 0.0;
80 }
81
82 let intersection = (x2 - x1) * (y2 - y1);
83 let self_area = self.width * self.height;
84 let other_area = other.width * other.height;
85 let union = self_area + other_area - intersection;
86
87 if union <= 0.0 {
88 0.0
89 } else {
90 intersection / union
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
97pub struct Viewport {
98 pub width: u32,
99 pub height: u32,
100 pub device_pixel_ratio: f64,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "lowercase")]
106pub enum Modifier {
107 Shift,
108 Control,
109 Alt,
110 Meta,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(tag = "type", rename_all = "snake_case")]
116pub enum WaitCondition {
117 UrlChanged,
119 A11yContainsText { text: String },
121 ElementWithName {
123 name_contains: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 role: Option<String>,
126 },
127 PageLoaded,
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_bounds_center() {
137 let bounds = Bounds::new(100.0, 200.0, 50.0, 30.0);
138 let (cx, cy) = bounds.center();
139 assert_eq!(cx, 125.0);
140 assert_eq!(cy, 215.0);
141 }
142
143 #[test]
144 fn test_bounds_contains() {
145 let bounds = Bounds::new(100.0, 200.0, 50.0, 30.0);
146 assert!(bounds.contains(125.0, 215.0));
147 assert!(!bounds.contains(50.0, 215.0));
148 }
149
150 #[test]
151 fn test_bounds_iou() {
152 let a = Bounds::new(0.0, 0.0, 10.0, 10.0);
153 let b = Bounds::new(5.0, 5.0, 10.0, 10.0);
154 let iou = a.iou(&b);
155 assert!((iou - 25.0 / 175.0).abs() < 0.001);
157 }
158
159 #[test]
160 fn test_bounds_no_overlap() {
161 let a = Bounds::new(0.0, 0.0, 10.0, 10.0);
162 let b = Bounds::new(20.0, 20.0, 10.0, 10.0);
163 assert_eq!(a.iou(&b), 0.0);
164 assert!(!a.overlaps(&b));
165 }
166}