Skip to main content

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    /// Capture screenshot of visible tab.
142    #[serde(rename = "browsingContext.captureScreenshot")]
143    CaptureScreenshot {
144        /// Image format: "png" or "jpeg".
145        format: String,
146        /// Quality for JPEG (0-100), ignored for PNG.
147        #[serde(skip_serializing_if = "Option::is_none")]
148        quality: Option<u8>,
149    },
150}
151
152// ============================================================================
153// Element Commands
154// ============================================================================
155
156/// Element module commands for DOM interaction.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(tag = "method", content = "params")]
159pub enum ElementCommand {
160    /// Find single element by strategy.
161    #[serde(rename = "element.find")]
162    Find {
163        /// Selector strategy: "css", "xpath", "text", "partialText", "id", "tag", "name", "class", "linkText", "partialLinkText".
164        strategy: String,
165        /// Selector value.
166        value: String,
167        /// Parent element ID (optional).
168        #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
169        parent_id: Option<ElementId>,
170    },
171
172    /// Find all elements by strategy.
173    #[serde(rename = "element.findAll")]
174    FindAll {
175        /// Selector strategy.
176        strategy: String,
177        /// Selector value.
178        value: String,
179        /// Parent element ID (optional).
180        #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
181        parent_id: Option<ElementId>,
182    },
183
184    /// Get property via `element[name]`.
185    #[serde(rename = "element.getProperty")]
186    GetProperty {
187        /// Element ID.
188        #[serde(rename = "elementId")]
189        element_id: ElementId,
190        /// Property name.
191        name: String,
192    },
193
194    /// Set property via `element[name] = value`.
195    #[serde(rename = "element.setProperty")]
196    SetProperty {
197        /// Element ID.
198        #[serde(rename = "elementId")]
199        element_id: ElementId,
200        /// Property name.
201        name: String,
202        /// Property value.
203        value: Value,
204    },
205
206    /// Call method via `element[name](...args)`.
207    #[serde(rename = "element.callMethod")]
208    CallMethod {
209        /// Element ID.
210        #[serde(rename = "elementId")]
211        element_id: ElementId,
212        /// Method name.
213        name: String,
214        /// Method arguments.
215        #[serde(default)]
216        args: Vec<Value>,
217    },
218
219    /// Subscribe to element appearance.
220    #[serde(rename = "element.subscribe")]
221    Subscribe {
222        /// Selector strategy.
223        strategy: String,
224        /// Selector value.
225        value: String,
226        /// Auto-unsubscribe after first match.
227        #[serde(rename = "oneShot")]
228        one_shot: bool,
229        /// Timeout in milliseconds (optional).
230        #[serde(skip_serializing_if = "Option::is_none")]
231        timeout: Option<u64>,
232    },
233
234    /// Unsubscribe from element observation.
235    #[serde(rename = "element.unsubscribe")]
236    Unsubscribe {
237        /// Subscription ID.
238        #[serde(rename = "subscriptionId")]
239        subscription_id: String,
240    },
241
242    /// Watch for element removal.
243    #[serde(rename = "element.watchRemoval")]
244    WatchRemoval {
245        /// Element ID to watch.
246        #[serde(rename = "elementId")]
247        element_id: ElementId,
248    },
249
250    /// Stop watching for element removal.
251    #[serde(rename = "element.unwatchRemoval")]
252    UnwatchRemoval {
253        /// Element ID.
254        #[serde(rename = "elementId")]
255        element_id: ElementId,
256    },
257
258    /// Watch for attribute changes.
259    #[serde(rename = "element.watchAttribute")]
260    WatchAttribute {
261        /// Element ID.
262        #[serde(rename = "elementId")]
263        element_id: ElementId,
264        /// Specific attribute (optional).
265        #[serde(rename = "attributeName", skip_serializing_if = "Option::is_none")]
266        attribute_name: Option<String>,
267    },
268
269    /// Stop watching for attribute changes.
270    #[serde(rename = "element.unwatchAttribute")]
271    UnwatchAttribute {
272        /// Element ID.
273        #[serde(rename = "elementId")]
274        element_id: ElementId,
275    },
276
277    /// Capture screenshot of element.
278    #[serde(rename = "element.captureScreenshot")]
279    CaptureScreenshot {
280        /// Element ID.
281        #[serde(rename = "elementId")]
282        element_id: ElementId,
283        /// Image format: "png" or "jpeg".
284        format: String,
285        /// Quality for JPEG (0-100), ignored for PNG.
286        #[serde(skip_serializing_if = "Option::is_none")]
287        quality: Option<u8>,
288    },
289}
290
291// ============================================================================
292// Session Commands
293// ============================================================================
294
295/// Session module commands for connection management.
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(tag = "method", content = "params")]
298pub enum SessionCommand {
299    /// Get session status.
300    #[serde(rename = "session.status")]
301    Status,
302
303    /// Get and clear extension logs.
304    #[serde(rename = "session.stealLogs")]
305    StealLogs,
306
307    /// Subscribe to events.
308    #[serde(rename = "session.subscribe")]
309    Subscribe {
310        /// Event names to subscribe to.
311        events: Vec<String>,
312        /// CSS selectors for element events.
313        #[serde(skip_serializing_if = "Option::is_none")]
314        selectors: Option<Vec<String>>,
315    },
316
317    /// Unsubscribe from events.
318    #[serde(rename = "session.unsubscribe")]
319    Unsubscribe {
320        /// Subscription ID.
321        subscription_id: String,
322    },
323}
324
325// ============================================================================
326// Script Commands
327// ============================================================================
328
329/// Script module commands for JavaScript execution.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331#[serde(tag = "method", content = "params")]
332pub enum ScriptCommand {
333    /// Execute synchronous script.
334    #[serde(rename = "script.evaluate")]
335    Evaluate {
336        /// JavaScript code.
337        script: String,
338        /// Script arguments.
339        #[serde(default)]
340        args: Vec<Value>,
341    },
342
343    /// Execute async script.
344    #[serde(rename = "script.evaluateAsync")]
345    EvaluateAsync {
346        /// JavaScript code.
347        script: String,
348        /// Script arguments.
349        #[serde(default)]
350        args: Vec<Value>,
351    },
352
353    /// Add preload script.
354    #[serde(rename = "script.addPreloadScript")]
355    AddPreloadScript {
356        /// Script to run before page load.
357        script: String,
358    },
359
360    /// Remove preload script.
361    #[serde(rename = "script.removePreloadScript")]
362    RemovePreloadScript {
363        /// Script ID.
364        script_id: String,
365    },
366}
367
368// ============================================================================
369// Input Commands
370// ============================================================================
371
372/// Input module commands for keyboard and mouse simulation.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(tag = "method", content = "params")]
375pub enum InputCommand {
376    /// Type single key with modifiers.
377    #[serde(rename = "input.typeKey")]
378    TypeKey {
379        /// Element ID.
380        #[serde(rename = "elementId")]
381        element_id: ElementId,
382        /// Key value (e.g., "a", "Enter").
383        key: String,
384        /// Key code (e.g., "KeyA", "Enter").
385        code: String,
386        /// Legacy keyCode number.
387        #[serde(rename = "keyCode")]
388        key_code: u32,
389        /// Is printable character.
390        printable: bool,
391        /// Ctrl modifier.
392        #[serde(default)]
393        ctrl: bool,
394        /// Shift modifier.
395        #[serde(default)]
396        shift: bool,
397        /// Alt modifier.
398        #[serde(default)]
399        alt: bool,
400        /// Meta modifier.
401        #[serde(default)]
402        meta: bool,
403    },
404
405    /// Type text string character by character.
406    #[serde(rename = "input.typeText")]
407    TypeText {
408        /// Element ID.
409        #[serde(rename = "elementId")]
410        element_id: ElementId,
411        /// Text to type.
412        text: String,
413    },
414
415    /// Mouse click.
416    #[serde(rename = "input.mouseClick")]
417    MouseClick {
418        /// Element ID (optional).
419        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
420        element_id: Option<ElementId>,
421        /// X coordinate.
422        #[serde(skip_serializing_if = "Option::is_none")]
423        x: Option<i32>,
424        /// Y coordinate.
425        #[serde(skip_serializing_if = "Option::is_none")]
426        y: Option<i32>,
427        /// Mouse button (0=left, 1=middle, 2=right).
428        #[serde(default)]
429        button: u8,
430    },
431
432    /// Mouse move.
433    #[serde(rename = "input.mouseMove")]
434    MouseMove {
435        /// Element ID (optional).
436        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
437        element_id: Option<ElementId>,
438        /// X coordinate.
439        #[serde(skip_serializing_if = "Option::is_none")]
440        x: Option<i32>,
441        /// Y coordinate.
442        #[serde(skip_serializing_if = "Option::is_none")]
443        y: Option<i32>,
444    },
445
446    /// Mouse button down.
447    #[serde(rename = "input.mouseDown")]
448    MouseDown {
449        /// Element ID (optional).
450        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
451        element_id: Option<ElementId>,
452        /// X coordinate.
453        #[serde(skip_serializing_if = "Option::is_none")]
454        x: Option<i32>,
455        /// Y coordinate.
456        #[serde(skip_serializing_if = "Option::is_none")]
457        y: Option<i32>,
458        /// Mouse button.
459        #[serde(default)]
460        button: u8,
461    },
462
463    /// Mouse button up.
464    #[serde(rename = "input.mouseUp")]
465    MouseUp {
466        /// Element ID (optional).
467        #[serde(rename = "elementId", skip_serializing_if = "Option::is_none")]
468        element_id: Option<ElementId>,
469        /// X coordinate.
470        #[serde(skip_serializing_if = "Option::is_none")]
471        x: Option<i32>,
472        /// Y coordinate.
473        #[serde(skip_serializing_if = "Option::is_none")]
474        y: Option<i32>,
475        /// Mouse button.
476        #[serde(default)]
477        button: u8,
478    },
479}
480
481// ============================================================================
482// Network Commands
483// ============================================================================
484
485/// Network module commands for request interception.
486#[derive(Debug, Clone, Serialize, Deserialize)]
487#[serde(tag = "method", content = "params")]
488pub enum NetworkCommand {
489    /// Add network intercept.
490    #[serde(rename = "network.addIntercept")]
491    AddIntercept {
492        /// Intercept requests.
493        #[serde(default, rename = "interceptRequests")]
494        intercept_requests: bool,
495        /// Intercept request headers.
496        #[serde(default, rename = "interceptRequestHeaders")]
497        intercept_request_headers: bool,
498        /// Intercept request body (read-only).
499        #[serde(default, rename = "interceptRequestBody")]
500        intercept_request_body: bool,
501        /// Intercept response headers.
502        #[serde(default, rename = "interceptResponses")]
503        intercept_responses: bool,
504        /// Intercept response body.
505        #[serde(default, rename = "interceptResponseBody")]
506        intercept_response_body: bool,
507        /// URL patterns to filter (glob-style wildcards). Only matching URLs are intercepted.
508        #[serde(rename = "urlPatterns", skip_serializing_if = "Option::is_none")]
509        url_patterns: Option<Vec<String>>,
510        /// Resource types to filter (e.g., "document", "script", "xhr"). Only matching types are intercepted.
511        #[serde(rename = "resourceTypes", skip_serializing_if = "Option::is_none")]
512        resource_types: Option<Vec<String>>,
513    },
514
515    /// Remove network intercept.
516    #[serde(rename = "network.removeIntercept")]
517    RemoveIntercept {
518        /// Intercept ID.
519        #[serde(rename = "interceptId")]
520        intercept_id: InterceptId,
521    },
522
523    /// Set URL block rules.
524    #[serde(rename = "network.setBlockRules")]
525    SetBlockRules {
526        /// URL patterns to block.
527        patterns: Vec<String>,
528    },
529
530    /// Clear all block rules.
531    #[serde(rename = "network.clearBlockRules")]
532    ClearBlockRules,
533}
534
535// ============================================================================
536// Proxy Commands
537// ============================================================================
538
539/// Proxy module commands for proxy configuration.
540#[derive(Debug, Clone, Serialize, Deserialize)]
541#[serde(tag = "method", content = "params")]
542pub enum ProxyCommand {
543    /// Set window-level proxy.
544    #[serde(rename = "proxy.setWindowProxy")]
545    SetWindowProxy {
546        /// Proxy type: http, https, socks4, socks5.
547        #[serde(rename = "type")]
548        proxy_type: String,
549        /// Proxy host.
550        host: String,
551        /// Proxy port.
552        port: u16,
553        /// Username (optional).
554        #[serde(skip_serializing_if = "Option::is_none")]
555        username: Option<String>,
556        /// Password (optional).
557        #[serde(skip_serializing_if = "Option::is_none")]
558        password: Option<String>,
559        /// Proxy DNS (SOCKS only).
560        #[serde(rename = "proxyDns", default)]
561        proxy_dns: bool,
562    },
563
564    /// Clear window-level proxy.
565    #[serde(rename = "proxy.clearWindowProxy")]
566    ClearWindowProxy,
567
568    /// Set tab-level proxy.
569    #[serde(rename = "proxy.setTabProxy")]
570    SetTabProxy {
571        /// Proxy type.
572        #[serde(rename = "type")]
573        proxy_type: String,
574        /// Proxy host.
575        host: String,
576        /// Proxy port.
577        port: u16,
578        /// Username (optional).
579        #[serde(skip_serializing_if = "Option::is_none")]
580        username: Option<String>,
581        /// Password (optional).
582        #[serde(skip_serializing_if = "Option::is_none")]
583        password: Option<String>,
584        /// Proxy DNS (SOCKS only).
585        #[serde(rename = "proxyDns", default)]
586        proxy_dns: bool,
587    },
588
589    /// Clear tab-level proxy.
590    #[serde(rename = "proxy.clearTabProxy")]
591    ClearTabProxy,
592}
593
594// ============================================================================
595// Storage Commands
596// ============================================================================
597
598/// Storage module commands for cookie management.
599#[derive(Debug, Clone, Serialize, Deserialize)]
600#[serde(tag = "method", content = "params")]
601pub enum StorageCommand {
602    /// Get cookie by name.
603    #[serde(rename = "storage.getCookie")]
604    GetCookie {
605        /// Cookie name.
606        name: String,
607        /// URL (optional).
608        #[serde(skip_serializing_if = "Option::is_none")]
609        url: Option<String>,
610    },
611
612    /// Set cookie.
613    #[serde(rename = "storage.setCookie")]
614    SetCookie {
615        /// Cookie data.
616        cookie: Cookie,
617        /// URL (optional).
618        #[serde(skip_serializing_if = "Option::is_none")]
619        url: Option<String>,
620    },
621
622    /// Delete cookie by name.
623    #[serde(rename = "storage.deleteCookie")]
624    DeleteCookie {
625        /// Cookie name.
626        name: String,
627        /// URL (optional).
628        #[serde(skip_serializing_if = "Option::is_none")]
629        url: Option<String>,
630    },
631
632    /// Get all cookies.
633    #[serde(rename = "storage.getAllCookies")]
634    GetAllCookies {
635        /// URL (optional).
636        #[serde(skip_serializing_if = "Option::is_none")]
637        url: Option<String>,
638    },
639}
640
641// ============================================================================
642// Cookie
643// ============================================================================
644
645/// Browser cookie with standard properties.
646#[derive(Debug, Clone, Serialize, Deserialize)]
647pub struct Cookie {
648    /// Cookie name.
649    pub name: String,
650    /// Cookie value.
651    pub value: String,
652    /// Domain.
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub domain: Option<String>,
655    /// Path.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub path: Option<String>,
658    /// Secure flag.
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub secure: Option<bool>,
661    /// HttpOnly flag.
662    #[serde(rename = "httpOnly", skip_serializing_if = "Option::is_none")]
663    pub http_only: Option<bool>,
664    /// SameSite attribute.
665    #[serde(rename = "sameSite", skip_serializing_if = "Option::is_none")]
666    pub same_site: Option<String>,
667    /// Expiration timestamp (seconds).
668    #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
669    pub expiration_date: Option<f64>,
670}
671
672impl Cookie {
673    /// Creates a new cookie with name and value.
674    #[inline]
675    #[must_use]
676    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
677        Self {
678            name: name.into(),
679            value: value.into(),
680            domain: None,
681            path: None,
682            secure: None,
683            http_only: None,
684            same_site: None,
685            expiration_date: None,
686        }
687    }
688
689    /// Sets the domain.
690    #[inline]
691    #[must_use]
692    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
693        self.domain = Some(domain.into());
694        self
695    }
696
697    /// Sets the path.
698    #[inline]
699    #[must_use]
700    pub fn with_path(mut self, path: impl Into<String>) -> Self {
701        self.path = Some(path.into());
702        self
703    }
704
705    /// Sets the secure flag.
706    #[inline]
707    #[must_use]
708    pub fn with_secure(mut self, secure: bool) -> Self {
709        self.secure = Some(secure);
710        self
711    }
712
713    /// Sets the httpOnly flag.
714    #[inline]
715    #[must_use]
716    pub fn with_http_only(mut self, http_only: bool) -> Self {
717        self.http_only = Some(http_only);
718        self
719    }
720
721    /// Sets the sameSite attribute.
722    #[inline]
723    #[must_use]
724    pub fn with_same_site(mut self, same_site: impl Into<String>) -> Self {
725        self.same_site = Some(same_site.into());
726        self
727    }
728
729    /// Sets the expiration date.
730    #[inline]
731    #[must_use]
732    pub fn with_expiration_date(mut self, expiration_date: f64) -> Self {
733        self.expiration_date = Some(expiration_date);
734        self
735    }
736}
737
738// ============================================================================
739// Tests
740// ============================================================================
741
742#[cfg(test)]
743mod tests {
744    use super::*;
745
746    #[test]
747    fn test_browsing_context_navigate() {
748        let cmd = BrowsingContextCommand::Navigate {
749            url: "https://example.com".to_string(),
750        };
751        let json = serde_json::to_string(&cmd).expect("serialize");
752        assert!(json.contains("browsingContext.navigate"));
753        assert!(json.contains("https://example.com"));
754    }
755
756    #[test]
757    fn test_element_find() {
758        let cmd = ElementCommand::Find {
759            strategy: "css".to_string(),
760            value: "button.submit".to_string(),
761            parent_id: None,
762        };
763        let json = serde_json::to_string(&cmd).expect("serialize");
764        assert!(json.contains("element.find"));
765        assert!(json.contains("button.submit"));
766    }
767
768    #[test]
769    fn test_element_get_property() {
770        let cmd = ElementCommand::GetProperty {
771            element_id: ElementId::new("test-uuid"),
772            name: "textContent".to_string(),
773        };
774        let json = serde_json::to_string(&cmd).expect("serialize");
775        assert!(json.contains("element.getProperty"));
776        assert!(json.contains("test-uuid"));
777        assert!(json.contains("textContent"));
778    }
779
780    #[test]
781    fn test_cookie_builder() {
782        let cookie = Cookie::new("session", "abc123")
783            .with_domain(".example.com")
784            .with_path("/")
785            .with_secure(true)
786            .with_http_only(true)
787            .with_same_site("strict");
788
789        assert_eq!(cookie.name, "session");
790        assert_eq!(cookie.value, "abc123");
791        assert_eq!(cookie.domain, Some(".example.com".to_string()));
792        assert_eq!(cookie.secure, Some(true));
793    }
794
795    #[test]
796    fn test_network_add_intercept() {
797        let cmd = NetworkCommand::AddIntercept {
798            intercept_requests: true,
799            intercept_request_headers: false,
800            intercept_request_body: false,
801            intercept_responses: false,
802            intercept_response_body: false,
803            url_patterns: None,
804            resource_types: None,
805        };
806        let json = serde_json::to_string(&cmd).expect("serialize");
807        assert!(json.contains("network.addIntercept"));
808        // Verify None fields are omitted
809        assert!(!json.contains("urlPatterns"));
810        assert!(!json.contains("resourceTypes"));
811    }
812
813    #[test]
814    fn test_network_add_intercept_with_filters() {
815        let cmd = NetworkCommand::AddIntercept {
816            intercept_requests: true,
817            intercept_request_headers: false,
818            intercept_request_body: false,
819            intercept_responses: false,
820            intercept_response_body: false,
821            url_patterns: Some(vec!["*api*".to_string(), "*example.com*".to_string()]),
822            resource_types: Some(vec!["xhr".to_string(), "fetch".to_string()]),
823        };
824        let json = serde_json::to_string(&cmd).expect("serialize");
825        assert!(json.contains("network.addIntercept"));
826        assert!(json.contains("urlPatterns"));
827        assert!(json.contains("resourceTypes"));
828        assert!(json.contains("*api*"));
829        assert!(json.contains("xhr"));
830    }
831
832    #[test]
833    fn test_browsing_context_capture_screenshot() {
834        let cmd = BrowsingContextCommand::CaptureScreenshot {
835            format: "png".to_string(),
836            quality: None,
837        };
838        let json = serde_json::to_string(&cmd).expect("serialize");
839        assert!(json.contains("browsingContext.captureScreenshot"));
840        assert!(json.contains("\"format\":\"png\""));
841
842        let cmd_jpeg = BrowsingContextCommand::CaptureScreenshot {
843            format: "jpeg".to_string(),
844            quality: Some(85),
845        };
846        let json_jpeg = serde_json::to_string(&cmd_jpeg).expect("serialize");
847        assert!(json_jpeg.contains("\"quality\":85"));
848    }
849
850    #[test]
851    fn test_element_capture_screenshot() {
852        let cmd = ElementCommand::CaptureScreenshot {
853            element_id: ElementId::new("elem-uuid"),
854            format: "png".to_string(),
855            quality: None,
856        };
857        let json = serde_json::to_string(&cmd).expect("serialize");
858        assert!(json.contains("element.captureScreenshot"));
859        assert!(json.contains("elem-uuid"));
860        assert!(json.contains("\"format\":\"png\""));
861    }
862}