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        /// CSS selector that matched.
213        selector: String,
214        /// Element ID.
215        element_id: String,
216        /// Subscription ID.
217        subscription_id: String,
218        /// Tab ID.
219        tab_id: u32,
220        /// Frame ID.
221        frame_id: u64,
222    },
223
224    /// Element removed from DOM.
225    ElementRemoved {
226        /// Element ID.
227        element_id: String,
228        /// Tab ID.
229        tab_id: u32,
230        /// Frame ID.
231        frame_id: u64,
232    },
233
234    /// Element attribute changed.
235    ElementAttributeChanged {
236        /// Element ID.
237        element_id: String,
238        /// Attribute name.
239        attribute_name: String,
240        /// Old value.
241        old_value: Option<String>,
242        /// New value.
243        new_value: Option<String>,
244        /// Tab ID.
245        tab_id: u32,
246        /// Frame ID.
247        frame_id: u64,
248    },
249
250    /// Network request about to be sent.
251    NetworkBeforeRequestSent {
252        /// Request ID.
253        request_id: String,
254        /// Request URL.
255        url: String,
256        /// HTTP method.
257        method: String,
258        /// Resource type.
259        resource_type: String,
260    },
261
262    /// Network response headers received.
263    NetworkResponseStarted {
264        /// Request ID.
265        request_id: String,
266        /// Request URL.
267        url: String,
268        /// HTTP status code.
269        status: u16,
270        /// HTTP status text.
271        status_text: String,
272    },
273
274    /// Network response completed.
275    NetworkResponseCompleted {
276        /// Request ID.
277        request_id: String,
278        /// Request URL.
279        url: String,
280        /// HTTP status code.
281        status: u16,
282    },
283
284    /// Unknown event type.
285    Unknown {
286        /// Event method.
287        method: String,
288        /// Event params.
289        params: Value,
290    },
291}
292
293// ============================================================================
294// Event Parsing Implementation
295// ============================================================================
296
297impl Event {
298    /// Internal parsing implementation.
299    fn parse_internal(&self) -> ParsedEvent {
300        match self.method.as_str() {
301            "browsingContext.navigationStarted" => ParsedEvent::BrowsingContextNavigationStarted {
302                tab_id: self.get_u32("tabId"),
303                frame_id: self.get_u64("frameId"),
304                url: self.get_string("url"),
305            },
306
307            "browsingContext.domContentLoaded" => ParsedEvent::BrowsingContextDomContentLoaded {
308                tab_id: self.get_u32("tabId"),
309                frame_id: self.get_u64("frameId"),
310                url: self.get_string("url"),
311            },
312
313            "browsingContext.load" => ParsedEvent::BrowsingContextLoad {
314                tab_id: self.get_u32("tabId"),
315                frame_id: self.get_u64("frameId"),
316                url: self.get_string("url"),
317            },
318
319            "browsingContext.navigationFailed" => ParsedEvent::BrowsingContextNavigationFailed {
320                tab_id: self.get_u32("tabId"),
321                frame_id: self.get_u64("frameId"),
322                url: self.get_string("url"),
323                error: self.get_string("error"),
324            },
325
326            "element.added" => ParsedEvent::ElementAdded {
327                selector: self.get_string("selector"),
328                element_id: self.get_string("elementId"),
329                subscription_id: self.get_string("subscriptionId"),
330                tab_id: self.get_u32("tabId"),
331                frame_id: self.get_u64("frameId"),
332            },
333
334            "element.removed" => ParsedEvent::ElementRemoved {
335                element_id: self.get_string("elementId"),
336                tab_id: self.get_u32("tabId"),
337                frame_id: self.get_u64("frameId"),
338            },
339
340            "element.attributeChanged" => ParsedEvent::ElementAttributeChanged {
341                element_id: self.get_string("elementId"),
342                attribute_name: self.get_string("attributeName"),
343                old_value: self.get_optional_string("oldValue"),
344                new_value: self.get_optional_string("newValue"),
345                tab_id: self.get_u32("tabId"),
346                frame_id: self.get_u64("frameId"),
347            },
348
349            "network.beforeRequestSent" => ParsedEvent::NetworkBeforeRequestSent {
350                request_id: self.get_string("requestId"),
351                url: self.get_string("url"),
352                method: self.get_string_or("method", "GET"),
353                resource_type: self.get_string_or("resourceType", "other"),
354            },
355
356            "network.responseStarted" => ParsedEvent::NetworkResponseStarted {
357                request_id: self.get_string("requestId"),
358                url: self.get_string("url"),
359                status: self.get_u16("status"),
360                status_text: self.get_string("statusText"),
361            },
362
363            "network.responseCompleted" => ParsedEvent::NetworkResponseCompleted {
364                request_id: self.get_string("requestId"),
365                url: self.get_string("url"),
366                status: self.get_u16("status"),
367            },
368
369            _ => ParsedEvent::Unknown {
370                method: self.method.clone(),
371                params: self.params.clone(),
372            },
373        }
374    }
375
376    /// Gets a string from params.
377    #[inline]
378    fn get_string(&self, key: &str) -> String {
379        self.params
380            .get(key)
381            .and_then(|v| v.as_str())
382            .unwrap_or_default()
383            .to_string()
384    }
385
386    /// Gets a string from params with default.
387    #[inline]
388    fn get_string_or(&self, key: &str, default: &str) -> String {
389        self.params
390            .get(key)
391            .and_then(|v| v.as_str())
392            .unwrap_or(default)
393            .to_string()
394    }
395
396    /// Gets an optional string from params.
397    #[inline]
398    fn get_optional_string(&self, key: &str) -> Option<String> {
399        self.params
400            .get(key)
401            .and_then(|v| v.as_str())
402            .map(|s| s.to_string())
403    }
404
405    /// Gets a u32 from params.
406    #[inline]
407    fn get_u32(&self, key: &str) -> u32 {
408        self.params
409            .get(key)
410            .and_then(|v| v.as_u64())
411            .unwrap_or_default() as u32
412    }
413
414    /// Gets a u64 from params.
415    #[inline]
416    fn get_u64(&self, key: &str) -> u64 {
417        self.params
418            .get(key)
419            .and_then(|v| v.as_u64())
420            .unwrap_or_default()
421    }
422
423    /// Gets a u16 from params.
424    #[inline]
425    fn get_u16(&self, key: &str) -> u16 {
426        self.params
427            .get(key)
428            .and_then(|v| v.as_u64())
429            .unwrap_or_default() as u16
430    }
431}
432
433// ============================================================================
434// Tests
435// ============================================================================
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_event_parsing() {
443        let json_str = r#"{
444            "id": "550e8400-e29b-41d4-a716-446655440000",
445            "type": "event",
446            "method": "browsingContext.load",
447            "params": {
448                "tabId": 1,
449                "frameId": 0,
450                "url": "https://example.com"
451            }
452        }"#;
453
454        let event: Event = serde_json::from_str(json_str).expect("parse event");
455        assert_eq!(event.module(), "browsingContext");
456        assert_eq!(event.event_name(), "load");
457
458        let parsed = event.parse();
459        match parsed {
460            ParsedEvent::BrowsingContextLoad {
461                tab_id,
462                frame_id,
463                url,
464            } => {
465                assert_eq!(tab_id, 1);
466                assert_eq!(frame_id, 0);
467                assert_eq!(url, "https://example.com");
468            }
469            _ => panic!("unexpected parsed event type"),
470        }
471    }
472
473    #[test]
474    fn test_event_reply_allow() {
475        let id = RequestId::generate();
476        let reply = EventReply::allow(id, "network.beforeRequestSent");
477        let json = serde_json::to_string(&reply).expect("serialize");
478
479        assert!(json.contains("replyTo"));
480        assert!(json.contains("allow"));
481    }
482
483    #[test]
484    fn test_event_reply_block() {
485        let id = RequestId::generate();
486        let reply = EventReply::block(id, "network.beforeRequestSent");
487        let json = serde_json::to_string(&reply).expect("serialize");
488
489        assert!(json.contains("block"));
490    }
491
492    #[test]
493    fn test_event_reply_redirect() {
494        let id = RequestId::generate();
495        let reply = EventReply::redirect(id, "network.beforeRequestSent", "https://other.com");
496        let json = serde_json::to_string(&reply).expect("serialize");
497
498        assert!(json.contains("redirect"));
499        assert!(json.contains("https://other.com"));
500    }
501
502    #[test]
503    fn test_element_added_parsing() {
504        let json_str = r##"{
505            "id": "550e8400-e29b-41d4-a716-446655440000",
506            "type": "event",
507            "method": "element.added",
508            "params": {
509                "selector": "#login-form",
510                "elementId": "elem-123",
511                "subscriptionId": "sub-456",
512                "tabId": 1,
513                "frameId": 0
514            }
515        }"##;
516
517        let event: Event = serde_json::from_str(json_str).expect("parse event");
518        let parsed = event.parse();
519
520        match parsed {
521            ParsedEvent::ElementAdded {
522                selector,
523                element_id,
524                ..
525            } => {
526                assert_eq!(selector, "#login-form");
527                assert_eq!(element_id, "elem-123");
528            }
529            _ => panic!("unexpected parsed event type"),
530        }
531    }
532
533    #[test]
534    fn test_unknown_event() {
535        let json_str = r#"{
536            "id": "550e8400-e29b-41d4-a716-446655440000",
537            "type": "event",
538            "method": "custom.unknownEvent",
539            "params": { "foo": "bar" }
540        }"#;
541
542        let event: Event = serde_json::from_str(json_str).expect("parse event");
543        let parsed = event.parse();
544
545        match parsed {
546            ParsedEvent::Unknown { method, .. } => {
547                assert_eq!(method, "custom.unknownEvent");
548            }
549            _ => panic!("expected Unknown variant"),
550        }
551    }
552}