1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "kind", rename_all = "camelCase")]
9pub enum BrowserMessage {
10 Register {
12 token: String,
13 session_id: Uuid,
14 url: String,
15 title: String,
16 user_agent: String,
17 top_origin: String,
18 },
19 Heartbeat { session_id: Uuid },
21 CommandResult {
23 session_id: Uuid,
24 command_id: String,
25 result: CommandResult,
26 },
27 Console {
29 session_id: Uuid,
30 events: Vec<ConsoleEvent>,
31 },
32 Network {
34 session_id: Uuid,
35 events: Vec<NetworkEvent>,
36 },
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(tag = "kind", rename_all = "camelCase")]
44pub enum DaemonMessage {
45 Metadata { session_id: Uuid, codename: String },
47 Command {
49 session_id: Uuid,
50 command: BrowserCommand,
51 },
52 Disconnect { reason: String },
54 Error { message: String },
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(tag = "type", rename_all = "camelCase")]
63pub enum BrowserCommand {
64 RunScript {
66 id: String,
67 code: String,
68 #[serde(default)]
69 capture_console: bool,
70 },
71 Screenshot {
73 id: String,
74 #[serde(default)]
75 selector: Option<String>,
76 #[serde(default = "default_quality")]
77 quality: f32,
78 #[serde(default)]
79 hooks: Vec<ScreenshotHook>,
80 },
81 Click { id: String, selector: String },
83 Navigate { id: String, url: String },
85 DiscoverSelectors { id: String },
87}
88
89fn default_quality() -> f32 {
90 0.85
91}
92
93impl BrowserCommand {
94 pub fn id(&self) -> &str {
95 match self {
96 BrowserCommand::RunScript { id, .. }
97 | BrowserCommand::Screenshot { id, .. }
98 | BrowserCommand::Click { id, .. }
99 | BrowserCommand::Navigate { id, .. }
100 | BrowserCommand::DiscoverSelectors { id, .. } => id,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(tag = "type", rename_all = "camelCase")]
108pub enum ScreenshotHook {
109 ScrollIntoView { selector: String },
110 WaitForSelector { selector: String, timeout_ms: u64 },
111 WaitForIdle { timeout_ms: u64 },
112 Wait { ms: u64 },
113 Script { code: String },
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct CommandResult {
121 pub ok: bool,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub data: Option<serde_json::Value>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub error: Option<String>,
126 pub duration_ms: u64,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ScreenshotData {
132 pub mime_type: String,
133 pub base64: String,
134 pub width: u32,
135 pub height: u32,
136 pub renderer: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct ConsoleEvent {
144 pub id: String,
145 pub timestamp: i64,
146 pub level: ConsoleLevel,
147 pub args: Vec<serde_json::Value>,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "lowercase")]
152pub enum ConsoleLevel {
153 Log,
154 Info,
155 Warn,
156 Error,
157 Debug,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct NetworkEvent {
163 pub id: String,
164 pub timestamp: i64,
165 pub method: String,
166 pub url: String,
167 pub status: Option<u16>,
168 pub duration_ms: Option<u64>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub error: Option<String>,
171}
172
173#[derive(Debug, Serialize, Deserialize)]
177pub struct HandshakeRequest {
178 pub app_label: String,
179}
180
181#[derive(Debug, Serialize, Deserialize)]
183pub struct HandshakeResponse {
184 pub session_id: Uuid,
185 pub session_token: String,
186 pub socket_url: String,
187 pub expires_at: i64,
188}
189
190#[derive(Debug, Serialize, Deserialize)]
192pub struct SessionInfo {
193 pub session_id: Uuid,
194 pub codename: String,
195 pub url: String,
196 pub title: String,
197 pub user_agent: String,
198 pub socket_state: SocketState,
199 pub connected_at: i64,
200 pub last_heartbeat: i64,
201 pub console_buffer_size: usize,
202 pub network_buffer_size: usize,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "lowercase")]
207pub enum SocketState {
208 Open,
209 Closed,
210}
211
212#[derive(Debug, Serialize, Deserialize)]
214pub struct CommandRequest {
215 pub command: BrowserCommand,
216 #[serde(default = "default_timeout")]
217 pub timeout_ms: u64,
218}
219
220fn default_timeout() -> u64 {
221 30_000
222}
223
224#[derive(Debug, Serialize, Deserialize)]
226pub struct CommandResponse {
227 pub result: CommandResult,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn browser_message_serialization() {
236 let msg = BrowserMessage::Register {
237 token: "test-token".into(),
238 session_id: Uuid::new_v4(),
239 url: "http://localhost:3000".into(),
240 title: "Test Page".into(),
241 user_agent: "Mozilla/5.0".into(),
242 top_origin: "http://localhost:3000".into(),
243 };
244
245 let json = serde_json::to_string(&msg).unwrap();
246 assert!(json.contains("\"kind\":\"register\""));
247
248 let decoded: BrowserMessage = serde_json::from_str(&json).unwrap();
249 assert!(matches!(decoded, BrowserMessage::Register { .. }));
250 }
251
252 #[test]
253 fn command_serialization() {
254 let cmd = BrowserCommand::RunScript {
255 id: "cmd-1".into(),
256 code: "document.title".into(),
257 capture_console: true,
258 };
259
260 let json = serde_json::to_string(&cmd).unwrap();
261 assert!(json.contains("\"type\":\"runScript\""));
262 }
263}