firefox_webdriver/browser/
tab.rs

1//! Browser tab automation and control.
2//!
3//! Each [`Tab`] represents a browser tab with a specific frame context.
4//!
5//! # Example
6//!
7//! ```ignore
8//! let tab = window.tab();
9//!
10//! // Navigate
11//! tab.goto("https://example.com").await?;
12//!
13//! // Find elements
14//! let button = tab.find_element("#submit").await?;
15//! button.click().await?;
16//!
17//! // Execute JavaScript
18//! let result = tab.execute_script("return document.title").await?;
19//! ```
20
21// ============================================================================
22// Imports
23// ============================================================================
24
25use std::fmt;
26use std::sync::Arc;
27use std::time::Duration;
28
29use base64::Engine;
30use base64::engine::general_purpose::STANDARD as Base64Standard;
31use parking_lot::Mutex as ParkingMutex;
32use serde_json::Value;
33use tokio::sync::oneshot;
34use tokio::time::timeout;
35use tracing::debug;
36
37use crate::error::{Error, Result};
38use crate::identifiers::{ElementId, FrameId, InterceptId, SessionId, SubscriptionId, TabId};
39use crate::protocol::event::ParsedEvent;
40use crate::protocol::{
41    BrowsingContextCommand, Command, Cookie, ElementCommand, Event, EventReply, NetworkCommand,
42    ProxyCommand, Request, Response, ScriptCommand, StorageCommand,
43};
44
45use super::network::{
46    BodyAction, HeadersAction, InterceptedRequest, InterceptedRequestBody,
47    InterceptedRequestHeaders, InterceptedResponse, InterceptedResponseBody, RequestAction,
48    RequestBody,
49};
50use super::proxy::ProxyConfig;
51use super::{Element, Window};
52
53// ============================================================================
54// Constants
55// ============================================================================
56
57/// Default timeout for wait_for_element (30 seconds).
58const DEFAULT_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
59
60// ============================================================================
61// Types
62// ============================================================================
63
64/// Information about a frame in the tab.
65#[derive(Debug, Clone)]
66pub struct FrameInfo {
67    /// Frame ID.
68    pub frame_id: FrameId,
69    /// Parent frame ID (None for main frame).
70    pub parent_frame_id: Option<FrameId>,
71    /// Frame URL.
72    pub url: String,
73}
74
75/// Internal shared state for a tab.
76pub(crate) struct TabInner {
77    /// Tab ID.
78    pub tab_id: TabId,
79    /// Current frame ID.
80    pub frame_id: FrameId,
81    /// Session ID.
82    pub session_id: SessionId,
83    /// Parent window (optional for standalone tab references).
84    pub window: Option<Window>,
85}
86
87// ============================================================================
88// Tab
89// ============================================================================
90
91/// A handle to a browser tab.
92///
93/// Tabs provide methods for navigation, scripting, and element interaction.
94#[derive(Clone)]
95pub struct Tab {
96    pub(crate) inner: Arc<TabInner>,
97}
98
99impl fmt::Debug for Tab {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.debug_struct("Tab")
102            .field("tab_id", &self.inner.tab_id)
103            .field("frame_id", &self.inner.frame_id)
104            .field("session_id", &self.inner.session_id)
105            .finish_non_exhaustive()
106    }
107}
108
109impl Tab {
110    /// Creates a new tab handle.
111    pub(crate) fn new(
112        tab_id: TabId,
113        frame_id: FrameId,
114        session_id: SessionId,
115        window: Option<Window>,
116    ) -> Self {
117        Self {
118            inner: Arc::new(TabInner {
119                tab_id,
120                frame_id,
121                session_id,
122                window,
123            }),
124        }
125    }
126}
127
128// ============================================================================
129// Tab - Accessors
130// ============================================================================
131
132impl Tab {
133    /// Returns the tab ID.
134    #[inline]
135    #[must_use]
136    pub fn tab_id(&self) -> TabId {
137        self.inner.tab_id
138    }
139
140    /// Returns the current frame ID.
141    #[inline]
142    #[must_use]
143    pub fn frame_id(&self) -> FrameId {
144        self.inner.frame_id
145    }
146
147    /// Returns the session ID.
148    #[inline]
149    #[must_use]
150    pub fn session_id(&self) -> SessionId {
151        self.inner.session_id
152    }
153}
154
155// ============================================================================
156// Tab - Navigation
157// ============================================================================
158
159impl Tab {
160    /// Navigates to a URL.
161    ///
162    /// # Arguments
163    ///
164    /// * `url` - The URL to navigate to
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if navigation fails.
169    pub async fn goto(&self, url: &str) -> Result<()> {
170        debug!(url = %url, tab_id = %self.inner.tab_id, "Navigating");
171
172        let command = Command::BrowsingContext(BrowsingContextCommand::Navigate {
173            url: url.to_string(),
174        });
175
176        self.send_command(command).await?;
177        Ok(())
178    }
179
180    /// Alias for [`goto`](Self::goto).
181    pub async fn navigate(&self, url: &str) -> Result<()> {
182        self.goto(url).await
183    }
184
185    /// Loads HTML content directly into the page.
186    ///
187    /// Useful for testing with inline HTML without needing a server.
188    ///
189    /// # Arguments
190    ///
191    /// * `html` - HTML content to load
192    ///
193    /// # Example
194    ///
195    /// ```ignore
196    /// tab.load_html("<html><body><h1>Test</h1></body></html>").await?;
197    /// ```
198    pub async fn load_html(&self, html: &str) -> Result<()> {
199        debug!(tab_id = %self.inner.tab_id, html_len = html.len(), "Loading HTML content");
200
201        let escaped_html = html
202            .replace('\\', "\\\\")
203            .replace('`', "\\`")
204            .replace("${", "\\${");
205
206        let script = format!(
207            r#"(function() {{
208                const html = `{}`;
209                const parser = new DOMParser();
210                const doc = parser.parseFromString(html, 'text/html');
211                const newTitle = doc.querySelector('title');
212                if (newTitle) {{ document.title = newTitle.textContent; }}
213                const newBody = doc.body;
214                if (newBody) {{
215                    document.body.innerHTML = newBody.innerHTML;
216                    for (const attr of newBody.attributes) {{
217                        document.body.setAttribute(attr.name, attr.value);
218                    }}
219                }}
220                const newHead = doc.head;
221                if (newHead) {{
222                    for (const child of newHead.children) {{
223                        if (child.tagName !== 'TITLE') {{
224                            document.head.appendChild(child.cloneNode(true));
225                        }}
226                    }}
227                }}
228            }})();"#,
229            escaped_html
230        );
231
232        self.execute_script(&script).await?;
233        Ok(())
234    }
235
236    /// Reloads the current page.
237    pub async fn reload(&self) -> Result<()> {
238        let command = Command::BrowsingContext(BrowsingContextCommand::Reload);
239        self.send_command(command).await?;
240        Ok(())
241    }
242
243    /// Navigates back in history.
244    pub async fn back(&self) -> Result<()> {
245        let command = Command::BrowsingContext(BrowsingContextCommand::GoBack);
246        self.send_command(command).await?;
247        Ok(())
248    }
249
250    /// Navigates forward in history.
251    pub async fn forward(&self) -> Result<()> {
252        let command = Command::BrowsingContext(BrowsingContextCommand::GoForward);
253        self.send_command(command).await?;
254        Ok(())
255    }
256
257    /// Gets the current page title.
258    pub async fn get_title(&self) -> Result<String> {
259        let command = Command::BrowsingContext(BrowsingContextCommand::GetTitle);
260        let response = self.send_command(command).await?;
261
262        let title = response
263            .result
264            .as_ref()
265            .and_then(|v| v.get("title"))
266            .and_then(|v| v.as_str())
267            .unwrap_or("")
268            .to_string();
269
270        Ok(title)
271    }
272
273    /// Gets the current URL.
274    pub async fn get_url(&self) -> Result<String> {
275        let command = Command::BrowsingContext(BrowsingContextCommand::GetUrl);
276        let response = self.send_command(command).await?;
277
278        let url = response
279            .result
280            .as_ref()
281            .and_then(|v| v.get("url"))
282            .and_then(|v| v.as_str())
283            .unwrap_or("")
284            .to_string();
285
286        Ok(url)
287    }
288
289    /// Focuses this tab (makes it active).
290    pub async fn focus(&self) -> Result<()> {
291        let command = Command::BrowsingContext(BrowsingContextCommand::FocusTab);
292        self.send_command(command).await?;
293        Ok(())
294    }
295
296    /// Focuses the window containing this tab.
297    pub async fn focus_window(&self) -> Result<()> {
298        let command = Command::BrowsingContext(BrowsingContextCommand::FocusWindow);
299        self.send_command(command).await?;
300        Ok(())
301    }
302
303    /// Closes this tab.
304    pub async fn close(&self) -> Result<()> {
305        let command = Command::BrowsingContext(BrowsingContextCommand::CloseTab);
306        self.send_command(command).await?;
307        Ok(())
308    }
309}
310
311// ============================================================================
312// Tab - Frame Switching
313// ============================================================================
314
315impl Tab {
316    /// Switches to a frame by iframe element.
317    ///
318    /// Returns a new Tab handle with the updated frame context.
319    ///
320    /// # Arguments
321    ///
322    /// * `iframe` - Element reference to an iframe
323    ///
324    /// # Example
325    ///
326    /// ```ignore
327    /// let iframe = tab.find_element("iframe#content").await?;
328    /// let frame_tab = tab.switch_to_frame(&iframe).await?;
329    /// ```
330    pub async fn switch_to_frame(&self, iframe: &Element) -> Result<Tab> {
331        debug!(tab_id = %self.inner.tab_id, element_id = %iframe.id(), "Switching to frame");
332
333        let command = Command::BrowsingContext(BrowsingContextCommand::SwitchToFrame {
334            element_id: iframe.id().clone(),
335        });
336        let response = self.send_command(command).await?;
337
338        let frame_id = extract_frame_id(&response)?;
339
340        Ok(Tab::new(
341            self.inner.tab_id,
342            FrameId::new(frame_id),
343            self.inner.session_id,
344            self.inner.window.clone(),
345        ))
346    }
347
348    /// Switches to a frame by index (0-based).
349    ///
350    /// # Arguments
351    ///
352    /// * `index` - Zero-based index of the frame
353    pub async fn switch_to_frame_by_index(&self, index: usize) -> Result<Tab> {
354        debug!(tab_id = %self.inner.tab_id, index, "Switching to frame by index");
355
356        let command =
357            Command::BrowsingContext(BrowsingContextCommand::SwitchToFrameByIndex { index });
358        let response = self.send_command(command).await?;
359
360        let frame_id = extract_frame_id(&response)?;
361
362        Ok(Tab::new(
363            self.inner.tab_id,
364            FrameId::new(frame_id),
365            self.inner.session_id,
366            self.inner.window.clone(),
367        ))
368    }
369
370    /// Switches to a frame by URL pattern.
371    ///
372    /// Supports wildcards (`*` for any characters, `?` for single character).
373    ///
374    /// # Arguments
375    ///
376    /// * `url_pattern` - URL pattern with optional wildcards
377    pub async fn switch_to_frame_by_url(&self, url_pattern: &str) -> Result<Tab> {
378        debug!(tab_id = %self.inner.tab_id, url_pattern, "Switching to frame by URL");
379
380        let command = Command::BrowsingContext(BrowsingContextCommand::SwitchToFrameByUrl {
381            url_pattern: url_pattern.to_string(),
382        });
383        let response = self.send_command(command).await?;
384
385        let frame_id = extract_frame_id(&response)?;
386
387        Ok(Tab::new(
388            self.inner.tab_id,
389            FrameId::new(frame_id),
390            self.inner.session_id,
391            self.inner.window.clone(),
392        ))
393    }
394
395    /// Switches to the parent frame.
396    pub async fn switch_to_parent_frame(&self) -> Result<Tab> {
397        debug!(tab_id = %self.inner.tab_id, "Switching to parent frame");
398
399        let command = Command::BrowsingContext(BrowsingContextCommand::SwitchToParentFrame);
400        let response = self.send_command(command).await?;
401
402        let frame_id = extract_frame_id(&response)?;
403
404        Ok(Tab::new(
405            self.inner.tab_id,
406            FrameId::new(frame_id),
407            self.inner.session_id,
408            self.inner.window.clone(),
409        ))
410    }
411
412    /// Switches to the main (top-level) frame.
413    #[must_use]
414    pub fn switch_to_main_frame(&self) -> Tab {
415        debug!(tab_id = %self.inner.tab_id, "Switching to main frame");
416
417        Tab::new(
418            self.inner.tab_id,
419            FrameId::main(),
420            self.inner.session_id,
421            self.inner.window.clone(),
422        )
423    }
424
425    /// Gets the count of direct child frames.
426    pub async fn get_frame_count(&self) -> Result<usize> {
427        let command = Command::BrowsingContext(BrowsingContextCommand::GetFrameCount);
428        let response = self.send_command(command).await?;
429
430        let count = response
431            .result
432            .as_ref()
433            .and_then(|v| v.get("count"))
434            .and_then(|v| v.as_u64())
435            .ok_or_else(|| Error::protocol("No count in response"))?;
436
437        Ok(count as usize)
438    }
439
440    /// Gets information about all frames in the tab.
441    pub async fn get_all_frames(&self) -> Result<Vec<FrameInfo>> {
442        let command = Command::BrowsingContext(BrowsingContextCommand::GetAllFrames);
443        let response = self.send_command(command).await?;
444
445        let frames = response
446            .result
447            .as_ref()
448            .and_then(|v| v.get("frames"))
449            .and_then(|v| v.as_array())
450            .map(|arr| arr.iter().filter_map(parse_frame_info).collect())
451            .unwrap_or_default();
452
453        Ok(frames)
454    }
455
456    /// Checks if currently in the main frame.
457    #[inline]
458    #[must_use]
459    pub fn is_main_frame(&self) -> bool {
460        self.inner.frame_id.is_main()
461    }
462}
463
464// ============================================================================
465// Tab - Network
466// ============================================================================
467
468impl Tab {
469    /// Sets URL patterns to block.
470    ///
471    /// Patterns support wildcards (`*`).
472    ///
473    /// # Example
474    ///
475    /// ```ignore
476    /// tab.set_block_rules(&["*ads*", "*tracking*"]).await?;
477    /// ```
478    pub async fn set_block_rules(&self, patterns: &[&str]) -> Result<()> {
479        debug!(tab_id = %self.inner.tab_id, pattern_count = patterns.len(), "Setting block rules");
480
481        let command = Command::Network(NetworkCommand::SetBlockRules {
482            patterns: patterns.iter().map(|s| (*s).to_string()).collect(),
483        });
484
485        self.send_command(command).await?;
486        Ok(())
487    }
488
489    /// Clears all URL block rules.
490    pub async fn clear_block_rules(&self) -> Result<()> {
491        let command = Command::Network(NetworkCommand::ClearBlockRules);
492        self.send_command(command).await?;
493        Ok(())
494    }
495
496    /// Intercepts network requests with a callback.
497    ///
498    /// # Returns
499    ///
500    /// An `InterceptId` that can be used to stop this intercept.
501    ///
502    /// # Example
503    ///
504    /// ```ignore
505    /// use firefox_webdriver::RequestAction;
506    ///
507    /// let id = tab.intercept_request(|req| {
508    ///     if req.url.contains("ads") {
509    ///         RequestAction::block()
510    ///     } else {
511    ///         RequestAction::allow()
512    ///     }
513    /// }).await?;
514    /// ```
515    pub async fn intercept_request<F>(&self, callback: F) -> Result<InterceptId>
516    where
517        F: Fn(InterceptedRequest) -> RequestAction + Send + Sync + 'static,
518    {
519        debug!(tab_id = %self.inner.tab_id, "Enabling request interception");
520
521        let window = self.get_window()?;
522        let callback = Arc::new(callback);
523
524        window.inner.pool.set_event_handler(
525            window.inner.session_id,
526            Box::new(move |event: Event| {
527                if event.method.as_str() != "network.beforeRequestSent" {
528                    return None;
529                }
530
531                let request = parse_intercepted_request(&event);
532                let action = callback(request);
533                let result = request_action_to_json(&action);
534
535                Some(EventReply::new(
536                    event.id,
537                    "network.beforeRequestSent",
538                    result,
539                ))
540            }),
541        );
542
543        let command = Command::Network(NetworkCommand::AddIntercept {
544            intercept_requests: true,
545            intercept_request_headers: false,
546            intercept_request_body: false,
547            intercept_responses: false,
548            intercept_response_body: false,
549        });
550
551        let response = self.send_command(command).await?;
552        extract_intercept_id(&response)
553    }
554
555    /// Intercepts request headers with a callback.
556    pub async fn intercept_request_headers<F>(&self, callback: F) -> Result<InterceptId>
557    where
558        F: Fn(InterceptedRequestHeaders) -> HeadersAction + Send + Sync + 'static,
559    {
560        debug!(tab_id = %self.inner.tab_id, "Enabling request headers interception");
561
562        let window = self.get_window()?;
563        let callback = Arc::new(callback);
564
565        window.inner.pool.set_event_handler(
566            window.inner.session_id,
567            Box::new(move |event: Event| {
568                if event.method.as_str() != "network.requestHeaders" {
569                    return None;
570                }
571
572                let headers_data = parse_intercepted_request_headers(&event);
573                let action = callback(headers_data);
574                let result = headers_action_to_json(&action);
575
576                Some(EventReply::new(event.id, "network.requestHeaders", result))
577            }),
578        );
579
580        let command = Command::Network(NetworkCommand::AddIntercept {
581            intercept_requests: false,
582            intercept_request_headers: true,
583            intercept_request_body: false,
584            intercept_responses: false,
585            intercept_response_body: false,
586        });
587
588        let response = self.send_command(command).await?;
589        extract_intercept_id(&response)
590    }
591
592    /// Intercepts request body for logging (read-only).
593    pub async fn intercept_request_body<F>(&self, callback: F) -> Result<InterceptId>
594    where
595        F: Fn(InterceptedRequestBody) + Send + Sync + 'static,
596    {
597        debug!(tab_id = %self.inner.tab_id, "Enabling request body interception");
598
599        let window = self.get_window()?;
600        let callback = Arc::new(callback);
601
602        window.inner.pool.set_event_handler(
603            window.inner.session_id,
604            Box::new(move |event: Event| {
605                if event.method.as_str() != "network.requestBody" {
606                    return None;
607                }
608
609                let body_data = parse_intercepted_request_body(&event);
610                callback(body_data);
611
612                Some(EventReply::new(
613                    event.id,
614                    "network.requestBody",
615                    serde_json::json!({ "action": "allow" }),
616                ))
617            }),
618        );
619
620        let command = Command::Network(NetworkCommand::AddIntercept {
621            intercept_requests: false,
622            intercept_request_headers: false,
623            intercept_request_body: true,
624            intercept_responses: false,
625            intercept_response_body: false,
626        });
627
628        let response = self.send_command(command).await?;
629        extract_intercept_id(&response)
630    }
631
632    /// Intercepts response headers with a callback.
633    pub async fn intercept_response<F>(&self, callback: F) -> Result<InterceptId>
634    where
635        F: Fn(InterceptedResponse) -> HeadersAction + Send + Sync + 'static,
636    {
637        debug!(tab_id = %self.inner.tab_id, "Enabling response interception");
638
639        let window = self.get_window()?;
640        let callback = Arc::new(callback);
641
642        window.inner.pool.set_event_handler(
643            window.inner.session_id,
644            Box::new(move |event: Event| {
645                if event.method.as_str() != "network.responseHeaders" {
646                    return None;
647                }
648
649                let resp = parse_intercepted_response(&event);
650                let action = callback(resp);
651                let result = headers_action_to_json(&action);
652
653                Some(EventReply::new(event.id, "network.responseHeaders", result))
654            }),
655        );
656
657        let command = Command::Network(NetworkCommand::AddIntercept {
658            intercept_requests: false,
659            intercept_request_headers: false,
660            intercept_request_body: false,
661            intercept_responses: true,
662            intercept_response_body: false,
663        });
664
665        let response = self.send_command(command).await?;
666        extract_intercept_id(&response)
667    }
668
669    /// Intercepts response body with a callback.
670    pub async fn intercept_response_body<F>(&self, callback: F) -> Result<InterceptId>
671    where
672        F: Fn(InterceptedResponseBody) -> BodyAction + Send + Sync + 'static,
673    {
674        debug!(tab_id = %self.inner.tab_id, "Enabling response body interception");
675
676        let window = self.get_window()?;
677        let callback = Arc::new(callback);
678
679        window.inner.pool.set_event_handler(
680            window.inner.session_id,
681            Box::new(move |event: Event| {
682                if event.method.as_str() != "network.responseBody" {
683                    return None;
684                }
685
686                let body_data = parse_intercepted_response_body(&event);
687                let action = callback(body_data);
688                let result = body_action_to_json(&action);
689
690                Some(EventReply::new(event.id, "network.responseBody", result))
691            }),
692        );
693
694        let command = Command::Network(NetworkCommand::AddIntercept {
695            intercept_requests: false,
696            intercept_request_headers: false,
697            intercept_request_body: false,
698            intercept_responses: false,
699            intercept_response_body: true,
700        });
701
702        let response = self.send_command(command).await?;
703        extract_intercept_id(&response)
704    }
705
706    /// Stops network interception.
707    ///
708    /// # Arguments
709    ///
710    /// * `intercept_id` - The intercept ID returned from intercept methods
711    pub async fn stop_intercept(&self, intercept_id: &InterceptId) -> Result<()> {
712        debug!(tab_id = %self.inner.tab_id, %intercept_id, "Stopping interception");
713
714        let window = self.get_window()?;
715        window
716            .inner
717            .pool
718            .clear_event_handler(window.inner.session_id);
719
720        let command = Command::Network(NetworkCommand::RemoveIntercept {
721            intercept_id: intercept_id.clone(),
722        });
723
724        self.send_command(command).await?;
725        Ok(())
726    }
727}
728
729// ============================================================================
730// Tab - Proxy
731// ============================================================================
732
733impl Tab {
734    /// Sets a proxy for this tab.
735    ///
736    /// Tab-level proxy overrides window-level proxy for this tab only.
737    ///
738    /// # Example
739    ///
740    /// ```ignore
741    /// use firefox_webdriver::ProxyConfig;
742    ///
743    /// tab.set_proxy(ProxyConfig::http("proxy.example.com", 8080)).await?;
744    /// ```
745    pub async fn set_proxy(&self, config: ProxyConfig) -> Result<()> {
746        debug!(tab_id = %self.inner.tab_id, proxy_type = %config.proxy_type.as_str(), "Setting proxy");
747
748        let command = Command::Proxy(ProxyCommand::SetTabProxy {
749            proxy_type: config.proxy_type.as_str().to_string(),
750            host: config.host,
751            port: config.port,
752            username: config.username,
753            password: config.password,
754            proxy_dns: config.proxy_dns,
755        });
756
757        self.send_command(command).await?;
758        Ok(())
759    }
760
761    /// Clears the proxy for this tab.
762    pub async fn clear_proxy(&self) -> Result<()> {
763        let command = Command::Proxy(ProxyCommand::ClearTabProxy);
764        self.send_command(command).await?;
765        Ok(())
766    }
767}
768
769// ============================================================================
770// Tab - Storage (Cookies)
771// ============================================================================
772
773impl Tab {
774    /// Gets a cookie by name.
775    pub async fn get_cookie(&self, name: &str) -> Result<Option<Cookie>> {
776        let command = Command::Storage(StorageCommand::GetCookie {
777            name: name.to_string(),
778            url: None,
779        });
780
781        let response = self.send_command(command).await?;
782
783        let cookie = response
784            .result
785            .as_ref()
786            .and_then(|v| v.get("cookie"))
787            .and_then(|v| serde_json::from_value::<Cookie>(v.clone()).ok());
788
789        Ok(cookie)
790    }
791
792    /// Sets a cookie.
793    ///
794    /// # Example
795    ///
796    /// ```ignore
797    /// use firefox_webdriver::Cookie;
798    ///
799    /// tab.set_cookie(Cookie::new("session", "abc123")).await?;
800    /// ```
801    pub async fn set_cookie(&self, cookie: Cookie) -> Result<()> {
802        let command = Command::Storage(StorageCommand::SetCookie { cookie, url: None });
803        self.send_command(command).await?;
804        Ok(())
805    }
806
807    /// Deletes a cookie by name.
808    pub async fn delete_cookie(&self, name: &str) -> Result<()> {
809        let command = Command::Storage(StorageCommand::DeleteCookie {
810            name: name.to_string(),
811            url: None,
812        });
813
814        self.send_command(command).await?;
815        Ok(())
816    }
817
818    /// Gets all cookies for the current page.
819    pub async fn get_all_cookies(&self) -> Result<Vec<Cookie>> {
820        let command = Command::Storage(StorageCommand::GetAllCookies { url: None });
821        let response = self.send_command(command).await?;
822
823        let cookies = response
824            .result
825            .as_ref()
826            .and_then(|v| v.get("cookies"))
827            .and_then(|v| v.as_array())
828            .map(|arr| {
829                arr.iter()
830                    .filter_map(|v| serde_json::from_value::<Cookie>(v.clone()).ok())
831                    .collect()
832            })
833            .unwrap_or_default();
834
835        Ok(cookies)
836    }
837}
838
839// ============================================================================
840// Tab - Storage (localStorage)
841// ============================================================================
842
843impl Tab {
844    /// Gets a value from localStorage.
845    pub async fn local_storage_get(&self, key: &str) -> Result<Option<String>> {
846        let script = format!("return localStorage.getItem({});", json_string(key));
847        let value = self.execute_script(&script).await?;
848
849        match value {
850            Value::Null => Ok(None),
851            Value::String(s) => Ok(Some(s)),
852            _ => Ok(value.as_str().map(|s| s.to_string())),
853        }
854    }
855
856    /// Sets a value in localStorage.
857    pub async fn local_storage_set(&self, key: &str, value: &str) -> Result<()> {
858        let script = format!(
859            "localStorage.setItem({}, {});",
860            json_string(key),
861            json_string(value)
862        );
863
864        self.execute_script(&script).await?;
865        Ok(())
866    }
867
868    /// Deletes a key from localStorage.
869    pub async fn local_storage_delete(&self, key: &str) -> Result<()> {
870        let script = format!("localStorage.removeItem({});", json_string(key));
871        self.execute_script(&script).await?;
872        Ok(())
873    }
874
875    /// Clears all localStorage.
876    pub async fn local_storage_clear(&self) -> Result<()> {
877        self.execute_script("localStorage.clear();").await?;
878        Ok(())
879    }
880}
881
882// ============================================================================
883// Tab - Storage (sessionStorage)
884// ============================================================================
885
886impl Tab {
887    /// Gets a value from sessionStorage.
888    pub async fn session_storage_get(&self, key: &str) -> Result<Option<String>> {
889        let script = format!("return sessionStorage.getItem({});", json_string(key));
890        let value = self.execute_script(&script).await?;
891
892        match value {
893            Value::Null => Ok(None),
894            Value::String(s) => Ok(Some(s)),
895            _ => Ok(value.as_str().map(|s| s.to_string())),
896        }
897    }
898
899    /// Sets a value in sessionStorage.
900    pub async fn session_storage_set(&self, key: &str, value: &str) -> Result<()> {
901        let script = format!(
902            "sessionStorage.setItem({}, {});",
903            json_string(key),
904            json_string(value)
905        );
906
907        self.execute_script(&script).await?;
908        Ok(())
909    }
910
911    /// Deletes a key from sessionStorage.
912    pub async fn session_storage_delete(&self, key: &str) -> Result<()> {
913        let script = format!("sessionStorage.removeItem({});", json_string(key));
914        self.execute_script(&script).await?;
915        Ok(())
916    }
917
918    /// Clears all sessionStorage.
919    pub async fn session_storage_clear(&self) -> Result<()> {
920        self.execute_script("sessionStorage.clear();").await?;
921        Ok(())
922    }
923}
924
925// ============================================================================
926// Tab - Script Execution
927// ============================================================================
928
929impl Tab {
930    /// Executes synchronous JavaScript in the page context.
931    ///
932    /// The script should use `return` to return a value.
933    ///
934    /// # Example
935    ///
936    /// ```ignore
937    /// let title = tab.execute_script("return document.title").await?;
938    /// ```
939    pub async fn execute_script(&self, script: &str) -> Result<Value> {
940        let command = Command::Script(ScriptCommand::Evaluate {
941            script: script.to_string(),
942            args: vec![],
943        });
944
945        let response = self.send_command(command).await?;
946
947        let value = response
948            .result
949            .as_ref()
950            .and_then(|v| v.get("value"))
951            .cloned()
952            .unwrap_or(Value::Null);
953
954        Ok(value)
955    }
956
957    /// Executes asynchronous JavaScript in the page context.
958    ///
959    /// The script should return a Promise or use async/await.
960    pub async fn execute_async_script(&self, script: &str) -> Result<Value> {
961        let command = Command::Script(ScriptCommand::EvaluateAsync {
962            script: script.to_string(),
963            args: vec![],
964        });
965
966        let response = self.send_command(command).await?;
967
968        let value = response
969            .result
970            .as_ref()
971            .and_then(|v| v.get("value"))
972            .cloned()
973            .unwrap_or(Value::Null);
974
975        Ok(value)
976    }
977}
978
979// ============================================================================
980// Tab - Element Search
981// ============================================================================
982
983impl Tab {
984    /// Finds a single element by CSS selector.
985    ///
986    /// # Errors
987    ///
988    /// Returns [`Error::ElementNotFound`] if no matching element exists.
989    pub async fn find_element(&self, selector: &str) -> Result<Element> {
990        let command = Command::Element(ElementCommand::Find {
991            selector: selector.to_string(),
992            parent_id: None,
993        });
994
995        let response = self.send_command(command).await?;
996
997        let element_id = response
998            .result
999            .as_ref()
1000            .and_then(|v| v.get("elementId"))
1001            .and_then(|v| v.as_str())
1002            .ok_or_else(|| {
1003                Error::element_not_found(selector, self.inner.tab_id, self.inner.frame_id)
1004            })?;
1005
1006        Ok(Element::new(
1007            ElementId::new(element_id),
1008            self.inner.tab_id,
1009            self.inner.frame_id,
1010            self.inner.session_id,
1011            self.inner.window.clone(),
1012        ))
1013    }
1014
1015    /// Finds all elements matching a CSS selector.
1016    pub async fn find_elements(&self, selector: &str) -> Result<Vec<Element>> {
1017        let command = Command::Element(ElementCommand::FindAll {
1018            selector: selector.to_string(),
1019            parent_id: None,
1020        });
1021
1022        let response = self.send_command(command).await?;
1023
1024        let elements = response
1025            .result
1026            .as_ref()
1027            .and_then(|v| v.get("elementIds"))
1028            .and_then(|v| v.as_array())
1029            .map(|arr| {
1030                arr.iter()
1031                    .filter_map(|v| v.as_str())
1032                    .map(|id| {
1033                        Element::new(
1034                            ElementId::new(id),
1035                            self.inner.tab_id,
1036                            self.inner.frame_id,
1037                            self.inner.session_id,
1038                            self.inner.window.clone(),
1039                        )
1040                    })
1041                    .collect()
1042            })
1043            .unwrap_or_default();
1044
1045        Ok(elements)
1046    }
1047}
1048
1049// ============================================================================
1050// Tab - Element Observation
1051// ============================================================================
1052
1053impl Tab {
1054    /// Waits for an element matching the selector to appear.
1055    ///
1056    /// Uses MutationObserver (no polling). Times out after 30 seconds.
1057    ///
1058    /// # Errors
1059    ///
1060    /// Returns `Timeout` if element doesn't appear within 30 seconds.
1061    pub async fn wait_for_element(&self, selector: &str) -> Result<Element> {
1062        self.wait_for_element_timeout(selector, DEFAULT_WAIT_TIMEOUT)
1063            .await
1064    }
1065
1066    /// Waits for an element with a custom timeout.
1067    ///
1068    /// # Arguments
1069    ///
1070    /// * `selector` - CSS selector to watch for
1071    /// * `timeout_duration` - Maximum time to wait
1072    pub async fn wait_for_element_timeout(
1073        &self,
1074        selector: &str,
1075        timeout_duration: Duration,
1076    ) -> Result<Element> {
1077        debug!(
1078            tab_id = %self.inner.tab_id,
1079            selector,
1080            timeout_ms = timeout_duration.as_millis(),
1081            "Waiting for element"
1082        );
1083
1084        let window = self.get_window()?;
1085
1086        let (tx, rx) = oneshot::channel::<Result<Element>>();
1087        let tx = Arc::new(ParkingMutex::new(Some(tx)));
1088        let selector_clone = selector.to_string();
1089        let tab_id = self.inner.tab_id;
1090        let frame_id = self.inner.frame_id;
1091        let session_id = self.inner.session_id;
1092        let window_clone = self.inner.window.clone();
1093        let tx_clone = Arc::clone(&tx);
1094
1095        window.inner.pool.set_event_handler(
1096            window.inner.session_id,
1097            Box::new(move |event: Event| {
1098                if event.method.as_str() != "element.added" {
1099                    return None;
1100                }
1101
1102                let parsed = event.parse();
1103                if let ParsedEvent::ElementAdded {
1104                    selector: event_selector,
1105                    element_id,
1106                    ..
1107                } = parsed
1108                    && event_selector == selector_clone
1109                {
1110                    let element = Element::new(
1111                        ElementId::new(&element_id),
1112                        tab_id,
1113                        frame_id,
1114                        session_id,
1115                        window_clone.clone(),
1116                    );
1117
1118                    if let Some(tx) = tx_clone.lock().take() {
1119                        let _ = tx.send(Ok(element));
1120                    }
1121                }
1122
1123                None
1124            }),
1125        );
1126
1127        let command = Command::Element(ElementCommand::Subscribe {
1128            selector: selector.to_string(),
1129            one_shot: true,
1130        });
1131        let response = self.send_command(command).await?;
1132
1133        // Check if element already exists
1134        if let Some(element_id) = response
1135            .result
1136            .as_ref()
1137            .and_then(|v| v.get("elementId"))
1138            .and_then(|v| v.as_str())
1139        {
1140            window
1141                .inner
1142                .pool
1143                .clear_event_handler(window.inner.session_id);
1144
1145            return Ok(Element::new(
1146                ElementId::new(element_id),
1147                self.inner.tab_id,
1148                self.inner.frame_id,
1149                self.inner.session_id,
1150                self.inner.window.clone(),
1151            ));
1152        }
1153
1154        let result = timeout(timeout_duration, rx).await;
1155
1156        window
1157            .inner
1158            .pool
1159            .clear_event_handler(window.inner.session_id);
1160
1161        match result {
1162            Ok(Ok(element)) => element,
1163            Ok(Err(_)) => Err(Error::protocol("Channel closed unexpectedly")),
1164            Err(_) => Err(Error::Timeout {
1165                operation: format!("wait_for_element({})", selector),
1166                timeout_ms: timeout_duration.as_millis() as u64,
1167            }),
1168        }
1169    }
1170
1171    /// Registers a callback for when elements matching the selector appear.
1172    ///
1173    /// # Returns
1174    ///
1175    /// Subscription ID for later unsubscription.
1176    pub async fn on_element_added<F>(&self, selector: &str, callback: F) -> Result<SubscriptionId>
1177    where
1178        F: Fn(Element) + Send + Sync + 'static,
1179    {
1180        debug!(tab_id = %self.inner.tab_id, selector, "Subscribing to element.added");
1181
1182        let window = self.get_window()?;
1183
1184        let selector_clone = selector.to_string();
1185        let tab_id = self.inner.tab_id;
1186        let frame_id = self.inner.frame_id;
1187        let session_id = self.inner.session_id;
1188        let window_clone = self.inner.window.clone();
1189        let callback = Arc::new(callback);
1190
1191        window.inner.pool.set_event_handler(
1192            window.inner.session_id,
1193            Box::new(move |event: Event| {
1194                if event.method.as_str() != "element.added" {
1195                    return None;
1196                }
1197
1198                let parsed = event.parse();
1199                if let ParsedEvent::ElementAdded {
1200                    selector: event_selector,
1201                    element_id,
1202                    ..
1203                } = parsed
1204                    && event_selector == selector_clone
1205                {
1206                    let element = Element::new(
1207                        ElementId::new(&element_id),
1208                        tab_id,
1209                        frame_id,
1210                        session_id,
1211                        window_clone.clone(),
1212                    );
1213                    callback(element);
1214                }
1215
1216                None
1217            }),
1218        );
1219
1220        let command = Command::Element(ElementCommand::Subscribe {
1221            selector: selector.to_string(),
1222            one_shot: false,
1223        });
1224
1225        let response = self.send_command(command).await?;
1226
1227        let subscription_id = response
1228            .result
1229            .as_ref()
1230            .and_then(|v| v.get("subscriptionId"))
1231            .and_then(|v| v.as_str())
1232            .ok_or_else(|| Error::protocol("No subscriptionId in response"))?;
1233
1234        Ok(SubscriptionId::new(subscription_id))
1235    }
1236
1237    /// Registers a callback for when a specific element is removed.
1238    pub async fn on_element_removed<F>(&self, element_id: &ElementId, callback: F) -> Result<()>
1239    where
1240        F: Fn() + Send + Sync + 'static,
1241    {
1242        debug!(tab_id = %self.inner.tab_id, %element_id, "Watching for element removal");
1243
1244        let window = self.get_window()?;
1245
1246        let element_id_clone = element_id.as_str().to_string();
1247        let callback = Arc::new(callback);
1248
1249        window.inner.pool.set_event_handler(
1250            window.inner.session_id,
1251            Box::new(move |event: Event| {
1252                if event.method.as_str() != "element.removed" {
1253                    return None;
1254                }
1255
1256                let parsed = event.parse();
1257                if let ParsedEvent::ElementRemoved {
1258                    element_id: removed_id,
1259                    ..
1260                } = parsed
1261                    && removed_id == element_id_clone
1262                {
1263                    callback();
1264                }
1265
1266                None
1267            }),
1268        );
1269
1270        let command = Command::Element(ElementCommand::WatchRemoval {
1271            element_id: element_id.clone(),
1272        });
1273
1274        self.send_command(command).await?;
1275        Ok(())
1276    }
1277
1278    /// Unsubscribes from element observation.
1279    pub async fn unsubscribe(&self, subscription_id: &SubscriptionId) -> Result<()> {
1280        let command = Command::Element(ElementCommand::Unsubscribe {
1281            subscription_id: subscription_id.as_str().to_string(),
1282        });
1283
1284        self.send_command(command).await?;
1285
1286        if let Some(window) = &self.inner.window {
1287            window
1288                .inner
1289                .pool
1290                .clear_event_handler(window.inner.session_id);
1291        }
1292
1293        Ok(())
1294    }
1295}
1296
1297// ============================================================================
1298// Tab - Internal
1299// ============================================================================
1300
1301impl Tab {
1302    /// Sends a command and returns the response.
1303    pub(crate) async fn send_command(&self, command: Command) -> Result<Response> {
1304        let window = self.get_window()?;
1305        let request = Request::new(self.inner.tab_id, self.inner.frame_id, command);
1306        window
1307            .inner
1308            .pool
1309            .send(window.inner.session_id, request)
1310            .await
1311    }
1312
1313    /// Gets the window reference or returns an error.
1314    fn get_window(&self) -> Result<&Window> {
1315        self.inner
1316            .window
1317            .as_ref()
1318            .ok_or_else(|| Error::protocol("Tab has no associated window"))
1319    }
1320}
1321
1322// ============================================================================
1323// Private Helpers
1324// ============================================================================
1325
1326/// Escapes a string for safe use in JavaScript.
1327fn json_string(s: &str) -> String {
1328    serde_json::to_string(s).unwrap_or_else(|_| format!("\"{}\"", s))
1329}
1330
1331/// Extracts frame ID from response.
1332fn extract_frame_id(response: &Response) -> Result<u64> {
1333    response
1334        .result
1335        .as_ref()
1336        .and_then(|v| v.get("frameId"))
1337        .and_then(|v| v.as_u64())
1338        .ok_or_else(|| Error::protocol("No frameId in response"))
1339}
1340
1341/// Extracts intercept ID from response.
1342fn extract_intercept_id(response: &Response) -> Result<InterceptId> {
1343    let id = response
1344        .result
1345        .as_ref()
1346        .and_then(|v| v.get("interceptId"))
1347        .and_then(|v| v.as_str())
1348        .ok_or_else(|| Error::protocol("No interceptId in response"))?;
1349
1350    Ok(InterceptId::new(id))
1351}
1352
1353/// Parses frame info from JSON value.
1354fn parse_frame_info(v: &Value) -> Option<FrameInfo> {
1355    Some(FrameInfo {
1356        frame_id: FrameId::new(v.get("frameId")?.as_u64()?),
1357        parent_frame_id: v
1358            .get("parentFrameId")
1359            .and_then(|p| p.as_i64())
1360            .and_then(|p| {
1361                if p < 0 {
1362                    None
1363                } else {
1364                    Some(FrameId::new(p as u64))
1365                }
1366            }),
1367        url: v.get("url")?.as_str()?.to_string(),
1368    })
1369}
1370
1371/// Parses intercepted request from event.
1372fn parse_intercepted_request(event: &Event) -> InterceptedRequest {
1373    InterceptedRequest {
1374        request_id: event
1375            .params
1376            .get("requestId")
1377            .and_then(|v| v.as_str())
1378            .unwrap_or("")
1379            .to_string(),
1380        url: event
1381            .params
1382            .get("url")
1383            .and_then(|v| v.as_str())
1384            .unwrap_or("")
1385            .to_string(),
1386        method: event
1387            .params
1388            .get("method")
1389            .and_then(|v| v.as_str())
1390            .unwrap_or("GET")
1391            .to_string(),
1392        resource_type: event
1393            .params
1394            .get("resourceType")
1395            .and_then(|v| v.as_str())
1396            .unwrap_or("other")
1397            .to_string(),
1398        tab_id: event
1399            .params
1400            .get("tabId")
1401            .and_then(|v| v.as_u64())
1402            .unwrap_or(0) as u32,
1403        frame_id: event
1404            .params
1405            .get("frameId")
1406            .and_then(|v| v.as_u64())
1407            .unwrap_or(0),
1408        body: None,
1409    }
1410}
1411
1412/// Parses intercepted request headers from event.
1413fn parse_intercepted_request_headers(event: &Event) -> InterceptedRequestHeaders {
1414    InterceptedRequestHeaders {
1415        request_id: event
1416            .params
1417            .get("requestId")
1418            .and_then(|v| v.as_str())
1419            .unwrap_or("")
1420            .to_string(),
1421        url: event
1422            .params
1423            .get("url")
1424            .and_then(|v| v.as_str())
1425            .unwrap_or("")
1426            .to_string(),
1427        method: event
1428            .params
1429            .get("method")
1430            .and_then(|v| v.as_str())
1431            .unwrap_or("GET")
1432            .to_string(),
1433        headers: event
1434            .params
1435            .get("headers")
1436            .and_then(|v| v.as_object())
1437            .map(|obj| {
1438                obj.iter()
1439                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
1440                    .collect()
1441            })
1442            .unwrap_or_default(),
1443        tab_id: event
1444            .params
1445            .get("tabId")
1446            .and_then(|v| v.as_u64())
1447            .unwrap_or(0) as u32,
1448        frame_id: event
1449            .params
1450            .get("frameId")
1451            .and_then(|v| v.as_u64())
1452            .unwrap_or(0),
1453    }
1454}
1455
1456/// Parses intercepted request body from event.
1457fn parse_intercepted_request_body(event: &Event) -> InterceptedRequestBody {
1458    InterceptedRequestBody {
1459        request_id: event
1460            .params
1461            .get("requestId")
1462            .and_then(|v| v.as_str())
1463            .unwrap_or("")
1464            .to_string(),
1465        url: event
1466            .params
1467            .get("url")
1468            .and_then(|v| v.as_str())
1469            .unwrap_or("")
1470            .to_string(),
1471        method: event
1472            .params
1473            .get("method")
1474            .and_then(|v| v.as_str())
1475            .unwrap_or("GET")
1476            .to_string(),
1477        resource_type: event
1478            .params
1479            .get("resourceType")
1480            .and_then(|v| v.as_str())
1481            .unwrap_or("other")
1482            .to_string(),
1483        tab_id: event
1484            .params
1485            .get("tabId")
1486            .and_then(|v| v.as_u64())
1487            .unwrap_or(0) as u32,
1488        frame_id: event
1489            .params
1490            .get("frameId")
1491            .and_then(|v| v.as_u64())
1492            .unwrap_or(0),
1493        body: event.params.as_object().and_then(parse_request_body),
1494    }
1495}
1496
1497/// Parses intercepted response from event.
1498fn parse_intercepted_response(event: &Event) -> InterceptedResponse {
1499    InterceptedResponse {
1500        request_id: event
1501            .params
1502            .get("requestId")
1503            .and_then(|v| v.as_str())
1504            .unwrap_or("")
1505            .to_string(),
1506        url: event
1507            .params
1508            .get("url")
1509            .and_then(|v| v.as_str())
1510            .unwrap_or("")
1511            .to_string(),
1512        status: event
1513            .params
1514            .get("status")
1515            .and_then(|v| v.as_u64())
1516            .unwrap_or(0) as u16,
1517        status_text: event
1518            .params
1519            .get("statusText")
1520            .and_then(|v| v.as_str())
1521            .unwrap_or("")
1522            .to_string(),
1523        headers: event
1524            .params
1525            .get("headers")
1526            .and_then(|v| v.as_object())
1527            .map(|obj| {
1528                obj.iter()
1529                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
1530                    .collect()
1531            })
1532            .unwrap_or_default(),
1533        tab_id: event
1534            .params
1535            .get("tabId")
1536            .and_then(|v| v.as_u64())
1537            .unwrap_or(0) as u32,
1538        frame_id: event
1539            .params
1540            .get("frameId")
1541            .and_then(|v| v.as_u64())
1542            .unwrap_or(0),
1543    }
1544}
1545
1546/// Parses intercepted response body from event.
1547fn parse_intercepted_response_body(event: &Event) -> InterceptedResponseBody {
1548    InterceptedResponseBody {
1549        request_id: event
1550            .params
1551            .get("requestId")
1552            .and_then(|v| v.as_str())
1553            .unwrap_or("")
1554            .to_string(),
1555        url: event
1556            .params
1557            .get("url")
1558            .and_then(|v| v.as_str())
1559            .unwrap_or("")
1560            .to_string(),
1561        tab_id: event
1562            .params
1563            .get("tabId")
1564            .and_then(|v| v.as_u64())
1565            .unwrap_or(0) as u32,
1566        frame_id: event
1567            .params
1568            .get("frameId")
1569            .and_then(|v| v.as_u64())
1570            .unwrap_or(0),
1571        body: event
1572            .params
1573            .get("body")
1574            .and_then(|v| v.as_str())
1575            .unwrap_or("")
1576            .to_string(),
1577        content_length: event
1578            .params
1579            .get("contentLength")
1580            .and_then(|v| v.as_u64())
1581            .unwrap_or(0) as usize,
1582    }
1583}
1584
1585/// Parses request body from event params.
1586fn parse_request_body(params: &serde_json::Map<String, Value>) -> Option<RequestBody> {
1587    let body = params.get("body")?;
1588    let body_obj = body.as_object()?;
1589
1590    if let Some(error) = body_obj.get("error").and_then(|v| v.as_str()) {
1591        return Some(RequestBody::Error(error.to_string()));
1592    }
1593
1594    if let Some(form_data) = body_obj.get("data").and_then(|v| v.as_object())
1595        && body_obj.get("type").and_then(|v| v.as_str()) == Some("formData")
1596    {
1597        let mut map = std::collections::HashMap::new();
1598        for (key, value) in form_data {
1599            if let Some(arr) = value.as_array() {
1600                let values: Vec<String> = arr
1601                    .iter()
1602                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
1603                    .collect();
1604                map.insert(key.clone(), values);
1605            }
1606        }
1607        return Some(RequestBody::FormData(map));
1608    }
1609
1610    if let Some(raw_data) = body_obj.get("data").and_then(|v| v.as_array())
1611        && body_obj.get("type").and_then(|v| v.as_str()) == Some("raw")
1612    {
1613        let mut bytes = Vec::new();
1614        for item in raw_data {
1615            if let Some(obj) = item.as_object()
1616                && let Some(b64) = obj.get("data").and_then(|v| v.as_str())
1617                && let Ok(decoded) = Base64Standard.decode(b64)
1618            {
1619                bytes.extend(decoded);
1620            }
1621        }
1622        if !bytes.is_empty() {
1623            return Some(RequestBody::Raw(bytes));
1624        }
1625    }
1626
1627    None
1628}
1629
1630/// Converts request action to JSON.
1631fn request_action_to_json(action: &RequestAction) -> Value {
1632    match action {
1633        RequestAction::Allow => serde_json::json!({ "action": "allow" }),
1634        RequestAction::Block => serde_json::json!({ "action": "block" }),
1635        RequestAction::Redirect(url) => serde_json::json!({ "action": "redirect", "url": url }),
1636    }
1637}
1638
1639/// Converts headers action to JSON.
1640fn headers_action_to_json(action: &HeadersAction) -> Value {
1641    match action {
1642        HeadersAction::Allow => serde_json::json!({ "action": "allow" }),
1643        HeadersAction::ModifyHeaders(h) => {
1644            serde_json::json!({ "action": "modifyHeaders", "headers": h })
1645        }
1646    }
1647}
1648
1649/// Converts body action to JSON.
1650fn body_action_to_json(action: &BodyAction) -> Value {
1651    match action {
1652        BodyAction::Allow => serde_json::json!({ "action": "allow" }),
1653        BodyAction::ModifyBody(b) => serde_json::json!({ "action": "modifyBody", "body": b }),
1654    }
1655}
1656
1657// ============================================================================
1658// Tests
1659// ============================================================================
1660
1661#[cfg(test)]
1662mod tests {
1663    use super::Tab;
1664
1665    #[test]
1666    fn test_tab_is_clone() {
1667        fn assert_clone<T: Clone>() {}
1668        assert_clone::<Tab>();
1669    }
1670
1671    #[test]
1672    fn test_tab_is_debug() {
1673        fn assert_debug<T: std::fmt::Debug>() {}
1674        assert_debug::<Tab>();
1675    }
1676}