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#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct CookieParam {
134 pub name: String,
135 pub value: String,
136 pub domain: String,
137 #[serde(default = "default_path")]
138 pub path: String,
139 #[serde(default)]
140 pub secure: bool,
141 #[serde(default)]
142 pub http_only: bool,
143 #[serde(default)]
144 pub same_site: Option<String>,
145}
146
147fn default_path() -> String {
148 "/".to_string()
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_bounds_center() {
157 let bounds = Bounds::new(100.0, 200.0, 50.0, 30.0);
158 let (cx, cy) = bounds.center();
159 assert_eq!(cx, 125.0);
160 assert_eq!(cy, 215.0);
161 }
162
163 #[test]
164 fn test_bounds_contains() {
165 let bounds = Bounds::new(100.0, 200.0, 50.0, 30.0);
166 assert!(bounds.contains(125.0, 215.0));
167 assert!(!bounds.contains(50.0, 215.0));
168 }
169
170 #[test]
171 fn test_bounds_iou() {
172 let a = Bounds::new(0.0, 0.0, 10.0, 10.0);
173 let b = Bounds::new(5.0, 5.0, 10.0, 10.0);
174 let iou = a.iou(&b);
175 assert!((iou - 25.0 / 175.0).abs() < 0.001);
177 }
178
179 #[test]
180 fn test_bounds_no_overlap() {
181 let a = Bounds::new(0.0, 0.0, 10.0, 10.0);
182 let b = Bounds::new(20.0, 20.0, 10.0, 10.0);
183 assert_eq!(a.iou(&b), 0.0);
184 assert!(!a.overlaps(&b));
185 }
186}