firefox_webdriver/protocol/
event.rs

1//! Event message types.
2//!
3//! Events are notifications sent from the remote end (extension) to the
4//! local end (Rust) when browser activity occurs.
5//!
6//! See ARCHITECTURE.md Section 2.4-2.5 and Section 5 for specification.
7//!
8//! # Event Types
9//!
10//! | Module | Events |
11//! |--------|--------|
12//! | `browsingContext` | `load`, `domContentLoaded`, `navigationStarted`, `navigationFailed` |
13//! | `element` | `added`, `removed`, `attributeChanged` |
14//! | `network` | `beforeRequestSent`, `responseStarted`, `responseCompleted` |
15
16// ============================================================================
17// Imports
18// ============================================================================
19
20use serde::{Deserialize, Serialize};
21use serde_json::{Value, json};
22
23use crate::identifiers::RequestId;
24
25// ============================================================================
26// Event
27// ============================================================================
28
29/// An event notification from remote end to local end.
30///
31/// # Format
32///
33/// ```json
34/// {
35///   "id": "event-uuid",
36///   "type": "event",
37///   "method": "module.eventName",
38///   "params": { ... }
39/// }
40/// ```
41#[derive(Debug, Clone, Deserialize)]
42pub struct Event {
43    /// Unique identifier for EventReply correlation.
44    pub id: RequestId,
45
46    /// Event type marker (always "event").
47    #[serde(rename = "type")]
48    pub event_type: String,
49
50    /// Event name in `module.eventName` format.
51    pub method: String,
52
53    /// Event-specific data.
54    pub params: Value,
55}
56
57impl Event {
58    /// Returns the module name from the method.
59    ///
60    /// # Example
61    ///
62    /// ```ignore
63    /// let event = Event { method: "browsingContext.load".into(), .. };
64    /// assert_eq!(event.module(), "browsingContext");
65    /// ```
66    #[inline]
67    #[must_use]
68    pub fn module(&self) -> &str {
69        self.method.split('.').next().unwrap_or_default()
70    }
71
72    /// Returns the event name from the method.
73    ///
74    /// # Example
75    ///
76    /// ```ignore
77    /// let event = Event { method: "browsingContext.load".into(), .. };
78    /// assert_eq!(event.event_name(), "load");
79    /// ```
80    #[inline]
81    #[must_use]
82    pub fn event_name(&self) -> &str {
83        self.method.split('.').nth(1).unwrap_or_default()
84    }
85
86    /// Parses the event into a typed variant.
87    #[must_use]
88    pub fn parse(&self) -> ParsedEvent {
89        self.parse_internal()
90    }
91}
92
93// ============================================================================
94// EventReply
95// ============================================================================
96
97/// A reply from local end to remote end for events requiring a decision.
98///
99/// Used for network interception to allow/block/redirect requests.
100///
101/// # Format
102///
103/// ```json
104/// {
105///   "id": "event-uuid",
106///   "replyTo": "network.beforeRequestSent",
107///   "result": { "action": "block" }
108/// }
109/// ```
110#[derive(Debug, Clone, Serialize)]
111pub struct EventReply {
112    /// Matches the event's ID.
113    pub id: RequestId,
114
115    /// Event method being replied to.
116    #[serde(rename = "replyTo")]
117    pub reply_to: String,
118
119    /// Decision/action to take.
120    pub result: Value,
121}
122
123impl EventReply {
124    /// Creates a new event reply.
125    #[inline]
126    #[must_use]
127    pub fn new(id: RequestId, reply_to: impl Into<String>, result: Value) -> Self {
128        Self {
129            id,
130            reply_to: reply_to.into(),
131            result,
132        }
133    }
134
135    /// Creates an "allow" reply for network events.
136    #[inline]
137    #[must_use]
138    pub fn allow(id: RequestId, reply_to: impl Into<String>) -> Self {
139        Self::new(id, reply_to, json!({ "action": "allow" }))
140    }
141
142    /// Creates a "block" reply for network events.
143    #[inline]
144    #[must_use]
145    pub fn block(id: RequestId, reply_to: impl Into<String>) -> Self {
146        Self::new(id, reply_to, json!({ "action": "block" }))
147    }
148
149    /// Creates a "redirect" reply for network events.
150    #[inline]
151    #[must_use]
152    pub fn redirect(id: RequestId, reply_to: impl Into<String>, url: impl Into<String>) -> Self {
153        Self::new(
154            id,
155            reply_to,
156            json!({ "action": "redirect", "url": url.into() }),
157        )
158    }
159}
160
161// ============================================================================
162// ParsedEvent
163// ============================================================================
164
165/// Parsed event types for type-safe handling.
166#[derive(Debug, Clone)]
167pub enum ParsedEvent {
168    /// Navigation started.
169    BrowsingContextNavigationStarted {
170        /// Tab ID.
171        tab_id: u32,
172        /// Frame ID.
173        frame_id: u64,
174        /// Page URL.
175        url: String,
176    },
177
178    /// DOM content loaded.
179    BrowsingContextDomContentLoaded {
180        /// Tab ID.
181        tab_id: u32,
182        /// Frame ID.
183        frame_id: u64,
184        /// Page URL.
185        url: String,
186    },
187
188    /// Page load complete.
189    BrowsingContextLoad {
190        /// Tab ID.
191        tab_id: u32,
192        /// Frame ID.
193        frame_id: u64,
194        /// Page URL.
195        url: String,
196    },
197
198    /// Navigation failed.
199    BrowsingContextNavigationFailed {
200        /// Tab ID.
201        tab_id: u32,
202        /// Frame ID.
203        frame_id: u64,
204        /// Page URL.
205        url: String,
206        /// Error message.
207        error: String,
208    },
209
210    /// Element added to DOM.
211    ElementAdded {
212        /// Selector strategy (css, xpath, text, etc.).
213        strategy: String,
214        /// Selector value.
215        value: String,
216        /// Element ID.
217        element_id: String,
218        /// Subscription ID.
219        subscription_id: String,
220        /// Tab ID.
221        tab_id: u32,
222        /// Frame ID.
223        frame_id: u64,
224    },
225
226    /// Element removed from DOM.
227    ElementRemoved {
228        /// Element ID.
229        element_id: String,
230        /// Tab ID.
231        tab_id: u32,
232        /// Frame ID.
233        frame_id: u64,
234    },
235
236    /// Element attribute changed.
237    ElementAttributeChanged {
238        /// Element ID.
239        element_id: String,
240        /// Attribute name.
241        attribute_name: String,
242        /// Old value.
243        old_value: Option<String>,
244        /// New value.
245        new_value: Option<String>,
246        /// Tab ID.
247        tab_id: u32,
248        /// Frame ID.
249        frame_id: u64,
250    },
251
252    /// Network request about to be sent.
253    NetworkBeforeRequestSent {
254        /// Request ID.
255        request_id: String,
256        /// Request URL.
257        url: String,
258        /// HTTP method.
259        method: String,
260        /// Resource type.
261        resource_type: String,
262    },
263
264    /// Network response headers received.
265    NetworkResponseStarted {
266        /// Request ID.
267        request_id: String,
268        /// Request URL.
269        url: String,
270        /// HTTP status code.
271        status: u16,
272        /// HTTP status text.
273        status_text: String,
274    },
275
276    /// Network response completed.
277    NetworkResponseCompleted {
278        /// Request ID.
279        request_id: String,
280        /// Request URL.
281        url: String,
282        /// HTTP status code.
283        status: u16,
284    },
285
286    /// Unknown event type.
287    Unknown {
288        /// Event method.
289        method: String,
290        /// Event params.
291        params: Value,
292    },
293}
294
295// ============================================================================
296// Event Parsing Implementation
297// ============================================================================
298
299impl Event {
300    /// Internal parsing implementation.
301    fn parse_internal(&self) -> ParsedEvent {
302        match self.method.as_str() {
303            "browsingContext.navigationStarted" => ParsedEvent::BrowsingContextNavigationStarted {
304                tab_id: self.get_u32("tabId"),
305                frame_id: self.get_u64("frameId"),
306                url: self.get_string("url"),
307            },
308
309            "browsingContext.domContentLoaded" => ParsedEvent::BrowsingContextDomContentLoaded {
310                tab_id: self.get_u32("tabId"),
311                frame_id: self.get_u64("frameId"),
312                url: self.get_string("url"),
313            },
314
315            "browsingContext.load" => ParsedEvent::BrowsingContextLoad {
316                tab_id: self.get_u32("tabId"),
317                frame_id: self.get_u64("frameId"),
318                url: self.get_string("url"),
319            },
320
321            "browsingContext.navigationFailed" => ParsedEvent::BrowsingContextNavigationFailed {
322                tab_id: self.get_u32("tabId"),
323                frame_id: self.get_u64("frameId"),
324                url: self.get_string("url"),
325                error: self.get_string("error"),
326            },
327
328            "element.added" => ParsedEvent::ElementAdded {
329                strategy: self.get_string("strategy"),
330                value: self.get_string("value"),
331                element_id: self.get_string("elementId"),
332                subscription_id: self.get_string("subscriptionId"),
333                tab_id: self.get_u32("tabId"),
334                frame_id: self.get_u64("frameId"),
335            },
336
337            "element.removed" => ParsedEvent::ElementRemoved {
338                element_id: self.get_string("elementId"),
339                tab_id: self.get_u32("tabId"),
340                frame_id: self.get_u64("frameId"),
341            },
342
343            "element.attributeChanged" => ParsedEvent::ElementAttributeChanged {
344                element_id: self.get_string("elementId"),
345                attribute_name: self.get_string("attributeName"),
346                old_value: self.get_optional_string("oldValue"),
347                new_value: self.get_optional_string("newValue"),
348                tab_id: self.get_u32("tabId"),
349                frame_id: self.get_u64("frameId"),
350            },
351
352            "network.beforeRequestSent" => ParsedEvent::NetworkBeforeRequestSent {
353                request_id: self.get_string("requestId"),
354                url: self.get_string("url"),
355                method: self.get_string_or("method", "GET"),
356                resource_type: self.get_string_or("resourceType", "other"),
357            },
358
359            "network.responseStarted" => ParsedEvent::NetworkResponseStarted {
360                request_id: self.get_string("requestId"),
361                url: self.get_string("url"),
362                status: self.get_u16("status"),
363                status_text: self.get_string("statusText"),
364            },
365
366            "network.responseCompleted" => ParsedEvent::NetworkResponseCompleted {
367                request_id: self.get_string("requestId"),
368                url: self.get_string("url"),
369                status: self.get_u16("status"),
370            },
371
372            _ => ParsedEvent::Unknown {
373                method: self.method.clone(),
374                params: self.params.clone(),
375            },
376        }
377    }
378
379    /// Gets a string from params.
380    #[inline]
381    fn get_string(&self, key: &str) -> String {
382        self.params
383            .get(key)
384            .and_then(|v| v.as_str())
385            .unwrap_or_default()
386            .to_string()
387    }
388
389    /// Gets a string from params with default.
390    #[inline]
391    fn get_string_or(&self, key: &str, default: &str) -> String {
392        self.params
393            .get(key)
394            .and_then(|v| v.as_str())
395            .unwrap_or(default)
396            .to_string()
397    }
398
399    /// Gets an optional string from params.
400    #[inline]
401    fn get_optional_string(&self, key: &str) -> Option<String> {
402        self.params
403            .get(key)
404            .and_then(|v| v.as_str())
405            .map(|s| s.to_string())
406    }
407
408    /// Gets a u32 from params.
409    #[inline]
410    fn get_u32(&self, key: &str) -> u32 {
411        self.params
412            .get(key)
413            .and_then(|v| v.as_u64())
414            .unwrap_or_default() as u32
415    }
416
417    /// Gets a u64 from params.
418    #[inline]
419    fn get_u64(&self, key: &str) -> u64 {
420        self.params
421            .get(key)
422            .and_then(|v| v.as_u64())
423            .unwrap_or_default()
424    }
425
426    /// Gets a u16 from params.
427    #[inline]
428    fn get_u16(&self, key: &str) -> u16 {
429        self.params
430            .get(key)
431            .and_then(|v| v.as_u64())
432            .unwrap_or_default() as u16
433    }
434}
435
436// ============================================================================
437// Tests
438// ============================================================================
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_event_parsing() {
446        let json_str = r#"{
447            "id": "550e8400-e29b-41d4-a716-446655440000",
448            "type": "event",
449            "method": "browsingContext.load",
450            "params": {
451                "tabId": 1,
452                "frameId": 0,
453                "url": "https://example.com"
454            }
455        }"#;
456
457        let event: Event = serde_json::from_str(json_str).expect("parse event");
458        assert_eq!(event.module(), "browsingContext");
459        assert_eq!(event.event_name(), "load");
460
461        let parsed = event.parse();
462        match parsed {
463            ParsedEvent::BrowsingContextLoad {
464                tab_id,
465                frame_id,
466                url,
467            } => {
468                assert_eq!(tab_id, 1);
469                assert_eq!(frame_id, 0);
470                assert_eq!(url, "https://example.com");
471            }
472            _ => panic!("unexpected parsed event type"),
473        }
474    }
475
476    #[test]
477    fn test_event_reply_allow() {
478        let id = RequestId::generate();
479        let reply = EventReply::allow(id, "network.beforeRequestSent");
480        let json = serde_json::to_string(&reply).expect("serialize");
481
482        assert!(json.contains("replyTo"));
483        assert!(json.contains("allow"));
484    }
485
486    #[test]
487    fn test_event_reply_block() {
488        let id = RequestId::generate();
489        let reply = EventReply::block(id, "network.beforeRequestSent");
490        let json = serde_json::to_string(&reply).expect("serialize");
491
492        assert!(json.contains("block"));
493    }
494
495    #[test]
496    fn test_event_reply_redirect() {
497        let id = RequestId::generate();
498        let reply = EventReply::redirect(id, "network.beforeRequestSent", "https://other.com");
499        let json = serde_json::to_string(&reply).expect("serialize");
500
501        assert!(json.contains("redirect"));
502        assert!(json.contains("https://other.com"));
503    }
504
505    #[test]
506    fn test_element_added_parsing() {
507        let json_str = r##"{
508            "id": "550e8400-e29b-41d4-a716-446655440000",
509            "type": "event",
510            "method": "element.added",
511            "params": {
512                "strategy": "css",
513                "value": "#login-form",
514                "elementId": "elem-123",
515                "subscriptionId": "sub-456",
516                "tabId": 1,
517                "frameId": 0
518            }
519        }"##;
520
521        let event: Event = serde_json::from_str(json_str).expect("parse event");
522        let parsed = event.parse();
523
524        match parsed {
525            ParsedEvent::ElementAdded {
526                strategy,
527                value,
528                element_id,
529                ..
530            } => {
531                assert_eq!(strategy, "css");
532                assert_eq!(value, "#login-form");
533                assert_eq!(element_id, "elem-123");
534            }
535            _ => panic!("unexpected parsed event type"),
536        }
537    }
538
539    #[test]
540    fn test_unknown_event() {
541        let json_str = r#"{
542            "id": "550e8400-e29b-41d4-a716-446655440000",
543            "type": "event",
544            "method": "custom.unknownEvent",
545            "params": { "foo": "bar" }
546        }"#;
547
548        let event: Event = serde_json::from_str(json_str).expect("parse event");
549        let parsed = event.parse();
550
551        match parsed {
552            ParsedEvent::Unknown { method, .. } => {
553                assert_eq!(method, "custom.unknownEvent");
554            }
555            _ => panic!("expected Unknown variant"),
556        }
557    }
558}