firefox_webdriver/protocol/
command.rs

1//! Command definitions organized by module.
2//!
3//! Commands follow `module.methodName` format per ARCHITECTURE.md Section 2.2.
4//!
5//! # Command Modules
6//!
7//! | Module | Commands |
8//! |--------|----------|
9//! | `browsingContext` | Navigation, tabs, frames |
10//! | `element` | Find, properties, methods |
11//! | `script` | JavaScript execution |
12//! | `input` | Keyboard and mouse |
13//! | `network` | Interception, blocking |
14//! | `proxy` | Proxy configuration |
15//! | `storage` | Cookies |
16//! | `session` | Status, subscriptions |
17
18// ============================================================================
19// Imports
20// ============================================================================
21
22use serde::{Deserialize, Serialize};
23use serde_json::Value;
24
25use crate::identifiers::{ElementId, InterceptId};
26
27// ============================================================================
28// Command Wrapper
29// ============================================================================
30
31/// All protocol commands organized by module.
32///
33/// This enum wraps module-specific command enums for unified serialization.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum Command {
37    /// BrowsingContext module commands.
38    BrowsingContext(BrowsingContextCommand),
39    /// Element module commands.
40    Element(ElementCommand),
41    /// Session module commands.
42    Session(SessionCommand),
43    /// Script module commands.
44    Script(ScriptCommand),
45    /// Input module commands.
46    Input(InputCommand),
47    /// Network module commands.
48    Network(NetworkCommand),
49    /// Proxy module commands.
50    Proxy(ProxyCommand),
51    /// Storage module commands.
52    Storage(StorageCommand),
53}
54
55// ============================================================================
56// BrowsingContext Commands
57// ============================================================================
58
59/// BrowsingContext module commands for navigation and tab management.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(tag = "method", content = "params")]
62pub enum BrowsingContextCommand {
63    /// Navigate to URL.
64    #[serde(rename = "browsingContext.navigate")]
65    Navigate {
66        /// URL to navigate to.
67        url: String,
68    },
69
70    /// Reload current page.
71    #[serde(rename = "browsingContext.reload")]
72    Reload,
73
74    /// Navigate back in history.
75    #[serde(rename = "browsingContext.goBack")]
76    GoBack,
77
78    /// Navigate forward in history.
79    #[serde(rename = "browsingContext.goForward")]
80    GoForward,
81
82    /// Get page title.
83    #[serde(rename = "browsingContext.getTitle")]
84    GetTitle,
85
86    /// Get current URL.
87    #[serde(rename = "browsingContext.getUrl")]
88    GetUrl,
89
90    /// Create new tab.
91    #[serde(rename = "browsingContext.newTab")]
92    NewTab,
93
94    /// Close current tab.
95    #[serde(rename = "browsingContext.closeTab")]
96    CloseTab,
97
98    /// Focus tab (make active).
99    #[serde(rename = "browsingContext.focusTab")]
100    FocusTab,
101
102    /// Focus window (bring to front).
103    #[serde(rename = "browsingContext.focusWindow")]
104    FocusWindow,
105
106    /// Switch to frame by element reference.
107    #[serde(rename = "browsingContext.switchToFrame")]
108    SwitchToFrame {
109        /// Element ID of iframe.
110        #[serde(rename = "elementId")]
111        element_id: ElementId,
112    },
113
114    /// Switch to frame by index.
115    #[serde(rename = "browsingContext.switchToFrameByIndex")]
116    SwitchToFrameByIndex {
117        /// Zero-based frame index.
118        index: usize,
119    },
120
121    /// Switch to frame by URL pattern.
122    #[serde(rename = "browsingContext.switchToFrameByUrl")]
123    SwitchToFrameByUrl {
124        /// URL pattern with wildcards.
125        #[serde(rename = "urlPattern")]
126        url_pattern: String,
127    },
128
129    /// Switch to parent frame.
130    #[serde(rename = "browsingContext.switchToParentFrame")]
131    SwitchToParentFrame,
132
133    /// Get child frame count.
134    #[serde(rename = "browsingContext.getFrameCount")]
135    GetFrameCount,
136
137    /// Get all frames info.
138    #[serde(rename = "browsingContext.getAllFrames")]
139    GetAllFrames,
140}
141
142// ============================================================================
143// Element Commands
144// ============================================================================
145
146/// Element module commands for DOM interaction.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(tag = "method", content = "params")]
149pub enum ElementCommand {
150    /// Find single element by CSS selector.
151    #[serde(rename = "element.find")]
152    Find {
153        /// CSS selector.
154        selector: String,
155        /// Parent element ID (optional).
156        #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
157        parent_id: Option<ElementId>,
158    },
159
160    /// Find all elements by CSS selector.
161    #[serde(rename = "element.findAll")]
162    FindAll {
163        /// CSS selector.
164        selector: String,
165        /// Parent element ID (optional).
166        #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
167        parent_id: Option<ElementId>,
168    },
169
170    /// Get property via `element[name]`.
171    #[serde(rename = "element.getProperty")]
172    GetProperty {
173        /// Element ID.
174        #[serde(rename = "elementId")]
175        element_id: ElementId,
176        /// Property name.
177        name: String,
178    },
179
180    /// Set property via `element[name] = value`.
181    #[serde(rename = "element.setProperty")]
182    SetProperty {
183        /// Element ID.
184        #[serde(rename = "elementId")]
185        element_id: ElementId,
186        /// Property name.
187        name: String,
188        /// Property value.
189        value: Value,
190    },
191
192    /// Call method via `element[name](...args)`.
193    #[serde(rename = "element.callMethod")]
194    CallMethod {
195        /// Element ID.
196        #[serde(rename = "elementId")]
197        element_id: ElementId,
198        /// Method name.
199        name: String,
200        /// Method arguments.
201        #[serde(default)]
202        args: Vec<Value>,
203    },
204
205    /// Subscribe to element appearance.
206    #[serde(rename = "element.subscribe")]
207    Subscribe {
208        /// CSS selector to watch.
209        selector: String,
210        /// Auto-unsubscribe after first match.
211        #[serde(rename = "oneShot")]
212        one_shot: bool,
213    },
214
215    /// Unsubscribe from element observation.
216    #[serde(rename = "element.unsubscribe")]
217    Unsubscribe {
218        /// Subscription ID.
219        #[serde(rename = "subscriptionId")]
220        subscription_id: String,
221    },
222
223    /// Watch for element removal.
224    #[serde(rename = "element.watchRemoval")]
225    WatchRemoval {
226        /// Element ID to watch.
227        #[serde(rename = "elementId")]
228        element_id: ElementId,
229    },
230
231    /// Stop watching for element removal.
232    #[serde(rename = "element.unwatchRemoval")]
233    UnwatchRemoval {
234        /// Element ID.
235        #[serde(rename = "elementId")]
236        element_id: ElementId,
237    },
238
239    /// Watch for attribute changes.
240    #[serde(rename = "element.watchAttribute")]
241    WatchAttribute {
242        /// Element ID.
243        #[serde(rename = "elementId")]
244        element_id: ElementId,
245        /// Specific attribute (optional).
246        #[serde(rename = "attributeName", skip_serializing_if = "Option::is_none")]
247        attribute_name: Option<String>,
248    },
249
250    /// Stop watching for attribute changes.
251    #[serde(rename = "element.unwatchAttribute")]
252    UnwatchAttribute {
253        /// Element ID.
254        #[serde(rename = "elementId")]
255        element_id: ElementId,
256    },
257}
258
259// ============================================================================
260// Session Commands
261// ============================================================================
262
263/// Session module commands for connection management.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265#[serde(tag = "method", content = "params")]
266pub enum SessionCommand {
267    /// Get session status.
268    #[serde(rename = "session.status")]
269    Status,
270
271    /// Get and clear extension logs.
272    #[serde(rename = "session.stealLogs")]
273    StealLogs,
274
275    /// Subscribe to events.
276    #[serde(rename = "session.subscribe")]
277    Subscribe {
278        /// Event names to subscribe to.
279        events: Vec<String>,
280        /// CSS selectors for element events.
281        #[serde(skip_serializing_if = "Option::is_none")]
282        selectors: Option<Vec<String>>,
283    },
284
285    /// Unsubscribe from events.
286    #[serde(rename = "session.unsubscribe")]
287    Unsubscribe {
288        /// Subscription ID.
289        subscription_id: String,
290    },
291}
292
293// ============================================================================
294// Script Commands
295// ============================================================================
296
297/// Script module commands for JavaScript execution.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(tag = "method", content = "params")]
300pub enum ScriptCommand {
301    /// Execute synchronous script.
302    #[serde(rename = "script.evaluate")]
303    Evaluate {
304        /// JavaScript code.
305        script: String,
306        /// Script arguments.
307        #[serde(default)]
308        args: Vec<Value>,
309    },
310
311    /// Execute async script.
312    #[serde(rename = "script.evaluateAsync")]
313    EvaluateAsync {
314        /// JavaScript code.
315        script: String,
316        /// Script arguments.
317        #[serde(default)]
318        args: Vec<Value>,
319    },
320
321    /// Add preload script.
322    #[serde(rename = "script.addPreloadScript")]
323    AddPreloadScript {
324        /// Script to run before page load.
325        script: String,
326    },
327
328    /// Remove preload script.
329    #[serde(rename = "script.removePreloadScript")]
330    RemovePreloadScript {
331        /// Script ID.
332        script_id: String,
333    },
334}
335
336// ============================================================================
337// Input Commands
338// ============================================================================
339
340/// Input module commands for keyboard and mouse simulation.
341#[derive(Debug, Clone, Serialize, Deserialize)]
342#[serde(tag = "method", content = "params")]
343pub enum InputCommand {
344    /// Type single key with modifiers.
345    #[serde(rename = "input.typeKey")]
346    TypeKey {
347        /// Element ID.
348        #[serde(rename = "elementId")]
349        element_id: ElementId,
350        /// Key value (e.g., "a", "Enter").
351        key: String,
352        /// Key code (e.g., "KeyA", "Enter").
353        code: String,
354        /// Legacy keyCode number.
355        #[serde(rename = "keyCode")]
356        key_code: u32,
357        /// Is printable character.
358        printable: bool,
359        /// Ctrl modifier.
360        #[serde(default)]
361        ctrl: bool,
362        /// Shift modifier.
363        #[serde(default)]
364        shift: bool,
365        /// Alt modifier.
366        #[serde(default)]
367        alt: bool,
368        /// Meta modifier.
369        #[serde(default)]
370        meta: bool,
371    },
372
373    /// Type text string character by character.
374    #[serde(rename = "input.typeText")]
375    TypeText {
376        /// Element ID.
377        #[serde(rename = "elementId")]
378        element_id: ElementId,
379        /// Text to type.
380        text: String,
381    },
382
383    /// Mouse click.
384    #[serde(rename = "input.mouseClick")]
385    MouseClick {
386        /// Element ID (optional).
387        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
388        element_id: Option<ElementId>,
389        /// X coordinate.
390        #[serde(skip_serializing_if = "Option::is_none")]
391        x: Option<i32>,
392        /// Y coordinate.
393        #[serde(skip_serializing_if = "Option::is_none")]
394        y: Option<i32>,
395        /// Mouse button (0=left, 1=middle, 2=right).
396        #[serde(default)]
397        button: u8,
398    },
399
400    /// Mouse move.
401    #[serde(rename = "input.mouseMove")]
402    MouseMove {
403        /// Element ID (optional).
404        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
405        element_id: Option<ElementId>,
406        /// X coordinate.
407        #[serde(skip_serializing_if = "Option::is_none")]
408        x: Option<i32>,
409        /// Y coordinate.
410        #[serde(skip_serializing_if = "Option::is_none")]
411        y: Option<i32>,
412    },
413
414    /// Mouse button down.
415    #[serde(rename = "input.mouseDown")]
416    MouseDown {
417        /// Element ID (optional).
418        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
419        element_id: Option<ElementId>,
420        /// X coordinate.
421        #[serde(skip_serializing_if = "Option::is_none")]
422        x: Option<i32>,
423        /// Y coordinate.
424        #[serde(skip_serializing_if = "Option::is_none")]
425        y: Option<i32>,
426        /// Mouse button.
427        #[serde(default)]
428        button: u8,
429    },
430
431    /// Mouse button up.
432    #[serde(rename = "input.mouseUp")]
433    MouseUp {
434        /// Element ID (optional).
435        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
436        element_id: Option<ElementId>,
437        /// X coordinate.
438        #[serde(skip_serializing_if = "Option::is_none")]
439        x: Option<i32>,
440        /// Y coordinate.
441        #[serde(skip_serializing_if = "Option::is_none")]
442        y: Option<i32>,
443        /// Mouse button.
444        #[serde(default)]
445        button: u8,
446    },
447}
448
449// ============================================================================
450// Network Commands
451// ============================================================================
452
453/// Network module commands for request interception.
454#[derive(Debug, Clone, Serialize, Deserialize)]
455#[serde(tag = "method", content = "params")]
456pub enum NetworkCommand {
457    /// Add network intercept.
458    #[serde(rename = "network.addIntercept")]
459    AddIntercept {
460        /// Intercept requests.
461        #[serde(default, rename = "interceptRequests")]
462        intercept_requests: bool,
463        /// Intercept request headers.
464        #[serde(default, rename = "interceptRequestHeaders")]
465        intercept_request_headers: bool,
466        /// Intercept request body (read-only).
467        #[serde(default, rename = "interceptRequestBody")]
468        intercept_request_body: bool,
469        /// Intercept response headers.
470        #[serde(default, rename = "interceptResponses")]
471        intercept_responses: bool,
472        /// Intercept response body.
473        #[serde(default, rename = "interceptResponseBody")]
474        intercept_response_body: bool,
475    },
476
477    /// Remove network intercept.
478    #[serde(rename = "network.removeIntercept")]
479    RemoveIntercept {
480        /// Intercept ID.
481        #[serde(rename = "interceptId")]
482        intercept_id: InterceptId,
483    },
484
485    /// Set URL block rules.
486    #[serde(rename = "network.setBlockRules")]
487    SetBlockRules {
488        /// URL patterns to block.
489        patterns: Vec<String>,
490    },
491
492    /// Clear all block rules.
493    #[serde(rename = "network.clearBlockRules")]
494    ClearBlockRules,
495}
496
497// ============================================================================
498// Proxy Commands
499// ============================================================================
500
501/// Proxy module commands for proxy configuration.
502#[derive(Debug, Clone, Serialize, Deserialize)]
503#[serde(tag = "method", content = "params")]
504pub enum ProxyCommand {
505    /// Set window-level proxy.
506    #[serde(rename = "proxy.setWindowProxy")]
507    SetWindowProxy {
508        /// Proxy type: http, https, socks4, socks5.
509        #[serde(rename = "type")]
510        proxy_type: String,
511        /// Proxy host.
512        host: String,
513        /// Proxy port.
514        port: u16,
515        /// Username (optional).
516        #[serde(skip_serializing_if = "Option::is_none")]
517        username: Option<String>,
518        /// Password (optional).
519        #[serde(skip_serializing_if = "Option::is_none")]
520        password: Option<String>,
521        /// Proxy DNS (SOCKS only).
522        #[serde(rename = "proxyDns", default)]
523        proxy_dns: bool,
524    },
525
526    /// Clear window-level proxy.
527    #[serde(rename = "proxy.clearWindowProxy")]
528    ClearWindowProxy,
529
530    /// Set tab-level proxy.
531    #[serde(rename = "proxy.setTabProxy")]
532    SetTabProxy {
533        /// Proxy type.
534        #[serde(rename = "type")]
535        proxy_type: String,
536        /// Proxy host.
537        host: String,
538        /// Proxy port.
539        port: u16,
540        /// Username (optional).
541        #[serde(skip_serializing_if = "Option::is_none")]
542        username: Option<String>,
543        /// Password (optional).
544        #[serde(skip_serializing_if = "Option::is_none")]
545        password: Option<String>,
546        /// Proxy DNS (SOCKS only).
547        #[serde(rename = "proxyDns", default)]
548        proxy_dns: bool,
549    },
550
551    /// Clear tab-level proxy.
552    #[serde(rename = "proxy.clearTabProxy")]
553    ClearTabProxy,
554}
555
556// ============================================================================
557// Storage Commands
558// ============================================================================
559
560/// Storage module commands for cookie management.
561#[derive(Debug, Clone, Serialize, Deserialize)]
562#[serde(tag = "method", content = "params")]
563pub enum StorageCommand {
564    /// Get cookie by name.
565    #[serde(rename = "storage.getCookie")]
566    GetCookie {
567        /// Cookie name.
568        name: String,
569        /// URL (optional).
570        #[serde(skip_serializing_if = "Option::is_none")]
571        url: Option<String>,
572    },
573
574    /// Set cookie.
575    #[serde(rename = "storage.setCookie")]
576    SetCookie {
577        /// Cookie data.
578        cookie: Cookie,
579        /// URL (optional).
580        #[serde(skip_serializing_if = "Option::is_none")]
581        url: Option<String>,
582    },
583
584    /// Delete cookie by name.
585    #[serde(rename = "storage.deleteCookie")]
586    DeleteCookie {
587        /// Cookie name.
588        name: String,
589        /// URL (optional).
590        #[serde(skip_serializing_if = "Option::is_none")]
591        url: Option<String>,
592    },
593
594    /// Get all cookies.
595    #[serde(rename = "storage.getAllCookies")]
596    GetAllCookies {
597        /// URL (optional).
598        #[serde(skip_serializing_if = "Option::is_none")]
599        url: Option<String>,
600    },
601}
602
603// ============================================================================
604// Cookie
605// ============================================================================
606
607/// Browser cookie with standard properties.
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct Cookie {
610    /// Cookie name.
611    pub name: String,
612    /// Cookie value.
613    pub value: String,
614    /// Domain.
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub domain: Option<String>,
617    /// Path.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub path: Option<String>,
620    /// Secure flag.
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub secure: Option<bool>,
623    /// HttpOnly flag.
624    #[serde(rename = "httpOnly", skip_serializing_if = "Option::is_none")]
625    pub http_only: Option<bool>,
626    /// SameSite attribute.
627    #[serde(rename = "sameSite", skip_serializing_if = "Option::is_none")]
628    pub same_site: Option<String>,
629    /// Expiration timestamp (seconds).
630    #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
631    pub expiration_date: Option<f64>,
632}
633
634impl Cookie {
635    /// Creates a new cookie with name and value.
636    #[inline]
637    #[must_use]
638    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
639        Self {
640            name: name.into(),
641            value: value.into(),
642            domain: None,
643            path: None,
644            secure: None,
645            http_only: None,
646            same_site: None,
647            expiration_date: None,
648        }
649    }
650
651    /// Sets the domain.
652    #[inline]
653    #[must_use]
654    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
655        self.domain = Some(domain.into());
656        self
657    }
658
659    /// Sets the path.
660    #[inline]
661    #[must_use]
662    pub fn with_path(mut self, path: impl Into<String>) -> Self {
663        self.path = Some(path.into());
664        self
665    }
666
667    /// Sets the secure flag.
668    #[inline]
669    #[must_use]
670    pub fn with_secure(mut self, secure: bool) -> Self {
671        self.secure = Some(secure);
672        self
673    }
674
675    /// Sets the httpOnly flag.
676    #[inline]
677    #[must_use]
678    pub fn with_http_only(mut self, http_only: bool) -> Self {
679        self.http_only = Some(http_only);
680        self
681    }
682
683    /// Sets the sameSite attribute.
684    #[inline]
685    #[must_use]
686    pub fn with_same_site(mut self, same_site: impl Into<String>) -> Self {
687        self.same_site = Some(same_site.into());
688        self
689    }
690
691    /// Sets the expiration date.
692    #[inline]
693    #[must_use]
694    pub fn with_expiration_date(mut self, expiration_date: f64) -> Self {
695        self.expiration_date = Some(expiration_date);
696        self
697    }
698}
699
700// ============================================================================
701// Tests
702// ============================================================================
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707
708    #[test]
709    fn test_browsing_context_navigate() {
710        let cmd = BrowsingContextCommand::Navigate {
711            url: "https://example.com".to_string(),
712        };
713        let json = serde_json::to_string(&cmd).expect("serialize");
714        assert!(json.contains("browsingContext.navigate"));
715        assert!(json.contains("https://example.com"));
716    }
717
718    #[test]
719    fn test_element_find() {
720        let cmd = ElementCommand::Find {
721            selector: "button.submit".to_string(),
722            parent_id: None,
723        };
724        let json = serde_json::to_string(&cmd).expect("serialize");
725        assert!(json.contains("element.find"));
726        assert!(json.contains("button.submit"));
727    }
728
729    #[test]
730    fn test_element_get_property() {
731        let cmd = ElementCommand::GetProperty {
732            element_id: ElementId::new("test-uuid"),
733            name: "textContent".to_string(),
734        };
735        let json = serde_json::to_string(&cmd).expect("serialize");
736        assert!(json.contains("element.getProperty"));
737        assert!(json.contains("test-uuid"));
738        assert!(json.contains("textContent"));
739    }
740
741    #[test]
742    fn test_cookie_builder() {
743        let cookie = Cookie::new("session", "abc123")
744            .with_domain(".example.com")
745            .with_path("/")
746            .with_secure(true)
747            .with_http_only(true)
748            .with_same_site("strict");
749
750        assert_eq!(cookie.name, "session");
751        assert_eq!(cookie.value, "abc123");
752        assert_eq!(cookie.domain, Some(".example.com".to_string()));
753        assert_eq!(cookie.secure, Some(true));
754    }
755
756    #[test]
757    fn test_network_add_intercept() {
758        let cmd = NetworkCommand::AddIntercept {
759            intercept_requests: true,
760            intercept_request_headers: false,
761            intercept_request_body: false,
762            intercept_responses: false,
763            intercept_response_body: false,
764        };
765        let json = serde_json::to_string(&cmd).expect("serialize");
766        assert!(json.contains("network.addIntercept"));
767    }
768}