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    },
508
509    /// Remove network intercept.
510    #[serde(rename = "network.removeIntercept")]
511    RemoveIntercept {
512        /// Intercept ID.
513        #[serde(rename = "interceptId")]
514        intercept_id: InterceptId,
515    },
516
517    /// Set URL block rules.
518    #[serde(rename = "network.setBlockRules")]
519    SetBlockRules {
520        /// URL patterns to block.
521        patterns: Vec<String>,
522    },
523
524    /// Clear all block rules.
525    #[serde(rename = "network.clearBlockRules")]
526    ClearBlockRules,
527}
528
529// ============================================================================
530// Proxy Commands
531// ============================================================================
532
533/// Proxy module commands for proxy configuration.
534#[derive(Debug, Clone, Serialize, Deserialize)]
535#[serde(tag = "method", content = "params")]
536pub enum ProxyCommand {
537    /// Set window-level proxy.
538    #[serde(rename = "proxy.setWindowProxy")]
539    SetWindowProxy {
540        /// Proxy type: http, https, socks4, socks5.
541        #[serde(rename = "type")]
542        proxy_type: String,
543        /// Proxy host.
544        host: String,
545        /// Proxy port.
546        port: u16,
547        /// Username (optional).
548        #[serde(skip_serializing_if = "Option::is_none")]
549        username: Option<String>,
550        /// Password (optional).
551        #[serde(skip_serializing_if = "Option::is_none")]
552        password: Option<String>,
553        /// Proxy DNS (SOCKS only).
554        #[serde(rename = "proxyDns", default)]
555        proxy_dns: bool,
556    },
557
558    /// Clear window-level proxy.
559    #[serde(rename = "proxy.clearWindowProxy")]
560    ClearWindowProxy,
561
562    /// Set tab-level proxy.
563    #[serde(rename = "proxy.setTabProxy")]
564    SetTabProxy {
565        /// Proxy type.
566        #[serde(rename = "type")]
567        proxy_type: String,
568        /// Proxy host.
569        host: String,
570        /// Proxy port.
571        port: u16,
572        /// Username (optional).
573        #[serde(skip_serializing_if = "Option::is_none")]
574        username: Option<String>,
575        /// Password (optional).
576        #[serde(skip_serializing_if = "Option::is_none")]
577        password: Option<String>,
578        /// Proxy DNS (SOCKS only).
579        #[serde(rename = "proxyDns", default)]
580        proxy_dns: bool,
581    },
582
583    /// Clear tab-level proxy.
584    #[serde(rename = "proxy.clearTabProxy")]
585    ClearTabProxy,
586}
587
588// ============================================================================
589// Storage Commands
590// ============================================================================
591
592/// Storage module commands for cookie management.
593#[derive(Debug, Clone, Serialize, Deserialize)]
594#[serde(tag = "method", content = "params")]
595pub enum StorageCommand {
596    /// Get cookie by name.
597    #[serde(rename = "storage.getCookie")]
598    GetCookie {
599        /// Cookie name.
600        name: String,
601        /// URL (optional).
602        #[serde(skip_serializing_if = "Option::is_none")]
603        url: Option<String>,
604    },
605
606    /// Set cookie.
607    #[serde(rename = "storage.setCookie")]
608    SetCookie {
609        /// Cookie data.
610        cookie: Cookie,
611        /// URL (optional).
612        #[serde(skip_serializing_if = "Option::is_none")]
613        url: Option<String>,
614    },
615
616    /// Delete cookie by name.
617    #[serde(rename = "storage.deleteCookie")]
618    DeleteCookie {
619        /// Cookie name.
620        name: String,
621        /// URL (optional).
622        #[serde(skip_serializing_if = "Option::is_none")]
623        url: Option<String>,
624    },
625
626    /// Get all cookies.
627    #[serde(rename = "storage.getAllCookies")]
628    GetAllCookies {
629        /// URL (optional).
630        #[serde(skip_serializing_if = "Option::is_none")]
631        url: Option<String>,
632    },
633}
634
635// ============================================================================
636// Cookie
637// ============================================================================
638
639/// Browser cookie with standard properties.
640#[derive(Debug, Clone, Serialize, Deserialize)]
641pub struct Cookie {
642    /// Cookie name.
643    pub name: String,
644    /// Cookie value.
645    pub value: String,
646    /// Domain.
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub domain: Option<String>,
649    /// Path.
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub path: Option<String>,
652    /// Secure flag.
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub secure: Option<bool>,
655    /// HttpOnly flag.
656    #[serde(rename = "httpOnly", skip_serializing_if = "Option::is_none")]
657    pub http_only: Option<bool>,
658    /// SameSite attribute.
659    #[serde(rename = "sameSite", skip_serializing_if = "Option::is_none")]
660    pub same_site: Option<String>,
661    /// Expiration timestamp (seconds).
662    #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
663    pub expiration_date: Option<f64>,
664}
665
666impl Cookie {
667    /// Creates a new cookie with name and value.
668    #[inline]
669    #[must_use]
670    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
671        Self {
672            name: name.into(),
673            value: value.into(),
674            domain: None,
675            path: None,
676            secure: None,
677            http_only: None,
678            same_site: None,
679            expiration_date: None,
680        }
681    }
682
683    /// Sets the domain.
684    #[inline]
685    #[must_use]
686    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
687        self.domain = Some(domain.into());
688        self
689    }
690
691    /// Sets the path.
692    #[inline]
693    #[must_use]
694    pub fn with_path(mut self, path: impl Into<String>) -> Self {
695        self.path = Some(path.into());
696        self
697    }
698
699    /// Sets the secure flag.
700    #[inline]
701    #[must_use]
702    pub fn with_secure(mut self, secure: bool) -> Self {
703        self.secure = Some(secure);
704        self
705    }
706
707    /// Sets the httpOnly flag.
708    #[inline]
709    #[must_use]
710    pub fn with_http_only(mut self, http_only: bool) -> Self {
711        self.http_only = Some(http_only);
712        self
713    }
714
715    /// Sets the sameSite attribute.
716    #[inline]
717    #[must_use]
718    pub fn with_same_site(mut self, same_site: impl Into<String>) -> Self {
719        self.same_site = Some(same_site.into());
720        self
721    }
722
723    /// Sets the expiration date.
724    #[inline]
725    #[must_use]
726    pub fn with_expiration_date(mut self, expiration_date: f64) -> Self {
727        self.expiration_date = Some(expiration_date);
728        self
729    }
730}
731
732// ============================================================================
733// Tests
734// ============================================================================
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739
740    #[test]
741    fn test_browsing_context_navigate() {
742        let cmd = BrowsingContextCommand::Navigate {
743            url: "https://example.com".to_string(),
744        };
745        let json = serde_json::to_string(&cmd).expect("serialize");
746        assert!(json.contains("browsingContext.navigate"));
747        assert!(json.contains("https://example.com"));
748    }
749
750    #[test]
751    fn test_element_find() {
752        let cmd = ElementCommand::Find {
753            strategy: "css".to_string(),
754            value: "button.submit".to_string(),
755            parent_id: None,
756        };
757        let json = serde_json::to_string(&cmd).expect("serialize");
758        assert!(json.contains("element.find"));
759        assert!(json.contains("button.submit"));
760    }
761
762    #[test]
763    fn test_element_get_property() {
764        let cmd = ElementCommand::GetProperty {
765            element_id: ElementId::new("test-uuid"),
766            name: "textContent".to_string(),
767        };
768        let json = serde_json::to_string(&cmd).expect("serialize");
769        assert!(json.contains("element.getProperty"));
770        assert!(json.contains("test-uuid"));
771        assert!(json.contains("textContent"));
772    }
773
774    #[test]
775    fn test_cookie_builder() {
776        let cookie = Cookie::new("session", "abc123")
777            .with_domain(".example.com")
778            .with_path("/")
779            .with_secure(true)
780            .with_http_only(true)
781            .with_same_site("strict");
782
783        assert_eq!(cookie.name, "session");
784        assert_eq!(cookie.value, "abc123");
785        assert_eq!(cookie.domain, Some(".example.com".to_string()));
786        assert_eq!(cookie.secure, Some(true));
787    }
788
789    #[test]
790    fn test_network_add_intercept() {
791        let cmd = NetworkCommand::AddIntercept {
792            intercept_requests: true,
793            intercept_request_headers: false,
794            intercept_request_body: false,
795            intercept_responses: false,
796            intercept_response_body: false,
797        };
798        let json = serde_json::to_string(&cmd).expect("serialize");
799        assert!(json.contains("network.addIntercept"));
800    }
801
802    #[test]
803    fn test_browsing_context_capture_screenshot() {
804        let cmd = BrowsingContextCommand::CaptureScreenshot {
805            format: "png".to_string(),
806            quality: None,
807        };
808        let json = serde_json::to_string(&cmd).expect("serialize");
809        assert!(json.contains("browsingContext.captureScreenshot"));
810        assert!(json.contains("\"format\":\"png\""));
811
812        let cmd_jpeg = BrowsingContextCommand::CaptureScreenshot {
813            format: "jpeg".to_string(),
814            quality: Some(85),
815        };
816        let json_jpeg = serde_json::to_string(&cmd_jpeg).expect("serialize");
817        assert!(json_jpeg.contains("\"quality\":85"));
818    }
819
820    #[test]
821    fn test_element_capture_screenshot() {
822        let cmd = ElementCommand::CaptureScreenshot {
823            element_id: ElementId::new("elem-uuid"),
824            format: "png".to_string(),
825            quality: None,
826        };
827        let json = serde_json::to_string(&cmd).expect("serialize");
828        assert!(json.contains("element.captureScreenshot"));
829        assert!(json.contains("elem-uuid"));
830        assert!(json.contains("\"format\":\"png\""));
831    }
832}