Skip to main content

playwright_rs/protocol/
frame.rs

1// Frame protocol object
2//
3// Represents a frame within a page. Pages have a main frame, and can have child frames (iframes).
4// Navigation and DOM operations happen on frames, not directly on pages.
5
6use crate::error::{Error, Result};
7use crate::protocol::page::{GotoOptions, Response, WaitUntil};
8use crate::protocol::{parse_result, serialize_argument, serialize_null};
9use crate::server::channel::Channel;
10use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
11use crate::server::connection::ConnectionExt;
12use serde::Deserialize;
13use serde_json::Value;
14use std::any::Any;
15use std::sync::{Arc, Mutex, RwLock};
16
17/// Frame represents a frame within a page.
18///
19/// Every page has a main frame, and pages can have additional child frames (iframes).
20/// Frame is where navigation, selector queries, and DOM operations actually happen.
21///
22/// In Playwright's architecture, Page delegates navigation and interaction methods to Frame.
23///
24/// See: <https://playwright.dev/docs/api/class-frame>
25#[derive(Clone)]
26pub struct Frame {
27    base: ChannelOwnerImpl,
28    /// Current URL of the frame.
29    /// Wrapped in RwLock to allow updates from events.
30    url: Arc<RwLock<String>>,
31    /// The name attribute of the frame element (empty string for the main frame).
32    /// Extracted from the protocol initializer.
33    name: Arc<str>,
34    /// GUID of the parent frame, if any (None for the main/top-level frame).
35    /// Extracted from the protocol initializer.
36    parent_frame_guid: Option<Arc<str>>,
37    /// Whether this frame has been detached from the page.
38    /// Set to true when a "detached" event is received.
39    is_detached: Arc<RwLock<bool>>,
40    /// The owning Page, set after the Page is created and the frame is adopted.
41    ///
42    /// This is `None` until `set_page()` is called by the owning Page.
43    /// Using `Mutex<Option<...>>` so that `set_page()` can be called on a shared `&Frame`.
44    page: Arc<Mutex<Option<crate::protocol::Page>>>,
45}
46
47impl Frame {
48    /// Creates a new Frame from protocol initialization.
49    ///
50    /// This is called by the object factory when the server sends a `__create__` message
51    /// for a Frame object.
52    pub fn new(
53        parent: Arc<dyn ChannelOwner>,
54        type_name: String,
55        guid: Arc<str>,
56        initializer: Value,
57    ) -> Result<Self> {
58        let base = ChannelOwnerImpl::new(
59            ParentOrConnection::Parent(parent),
60            type_name,
61            guid,
62            initializer.clone(),
63        );
64
65        // Extract initial URL from initializer if available
66        let initial_url = initializer
67            .get("url")
68            .and_then(|v| v.as_str())
69            .unwrap_or("about:blank")
70            .to_string();
71
72        let url = Arc::new(RwLock::new(initial_url));
73
74        // Extract the frame's name attribute (empty string for main frame)
75        let name: Arc<str> = Arc::from(
76            initializer
77                .get("name")
78                .and_then(|v| v.as_str())
79                .unwrap_or(""),
80        );
81
82        // Extract parent frame GUID if present
83        let parent_frame_guid: Option<Arc<str>> = initializer
84            .get("parentFrame")
85            .and_then(|v| v.get("guid"))
86            .and_then(|v| v.as_str())
87            .map(Arc::from);
88
89        Ok(Self {
90            base,
91            url,
92            name,
93            parent_frame_guid,
94            is_detached: Arc::new(RwLock::new(false)),
95            page: Arc::new(Mutex::new(None)),
96        })
97    }
98
99    /// Sets the owning Page for this frame.
100    ///
101    /// Called by `Page::main_frame()` after the frame is retrieved from the registry.
102    /// This allows `frame.page()` and `frame.locator()` to work.
103    pub(crate) fn set_page(&self, page: crate::protocol::Page) {
104        if let Ok(mut guard) = self.page.lock() {
105            *guard = Some(page);
106        }
107    }
108
109    /// Returns the owning Page for this frame, if it has been set.
110    ///
111    /// Returns `None` if `set_page()` has not been called yet (i.e., before the frame
112    /// has been adopted by a Page). In normal usage the main frame always has a Page.
113    ///
114    /// See: <https://playwright.dev/docs/api/class-frame#frame-page>
115    pub fn page(&self) -> Option<crate::protocol::Page> {
116        self.page.lock().ok().and_then(|g| g.clone())
117    }
118
119    /// Returns the `name` attribute value of the frame element used to create this frame.
120    ///
121    /// For the main (top-level) frame this is always an empty string.
122    ///
123    /// See: <https://playwright.dev/docs/api/class-frame#frame-name>
124    pub fn name(&self) -> &str {
125        &self.name
126    }
127
128    /// Returns the parent `Frame`, or `None` if this is the top-level (main) frame.
129    ///
130    /// See: <https://playwright.dev/docs/api/class-frame#frame-parent-frame>
131    pub fn parent_frame(&self) -> Option<crate::protocol::Frame> {
132        let guid = self.parent_frame_guid.as_ref()?;
133        // Look up the parent frame in the connection registry (sync-compatible via block_on)
134        // We spawn a brief async lookup using the connection.
135        let conn = self.base.connection();
136        // Use tokio's block_in_place / futures executor to do a synchronous resolution.
137        // This mirrors how other Rust Playwright clients resolve parent references.
138        tokio::task::block_in_place(|| {
139            tokio::runtime::Handle::current()
140                .block_on(conn.get_typed::<crate::protocol::Frame>(guid))
141                .ok()
142        })
143    }
144
145    /// Returns `true` if the frame has been detached from its page.
146    ///
147    /// A frame becomes detached when the corresponding `<iframe>` element is removed
148    /// from the DOM or when the owning page is closed.
149    ///
150    /// See: <https://playwright.dev/docs/api/class-frame#frame-is-detached>
151    pub fn is_detached(&self) -> bool {
152        self.is_detached.read().map(|v| *v).unwrap_or(false)
153    }
154
155    /// Returns all child frames embedded in this frame.
156    ///
157    /// Child frames are created by `<iframe>` elements within this frame.
158    /// For the main frame this may include multiple iframes.
159    ///
160    /// # Implementation Note
161    ///
162    /// This iterates all objects in the connection registry to find `Frame` objects
163    /// whose `parentFrame` initializer field matches this frame's GUID. This matches
164    /// the relationship Playwright establishes when creating child frames.
165    ///
166    /// See: <https://playwright.dev/docs/api/class-frame#frame-child-frames>
167    pub fn child_frames(&self) -> Vec<crate::protocol::Frame> {
168        let my_guid = self.guid().to_string();
169        let conn = self.base.connection();
170
171        // Use the synchronous registry snapshot — no async needed since the
172        // underlying storage is a parking_lot::Mutex (sync-safe to lock).
173        conn.all_objects_sync()
174            .into_iter()
175            .filter_map(|obj| {
176                // Only consider Frame-typed objects
177                if obj.type_name() != "Frame" {
178                    return None;
179                }
180                // Check the initializer's parentFrame.guid field
181                let parent_guid = obj
182                    .initializer()
183                    .get("parentFrame")
184                    .and_then(|v| v.get("guid"))
185                    .and_then(|v| v.as_str())?;
186
187                if parent_guid == my_guid {
188                    obj.as_any()
189                        .downcast_ref::<crate::protocol::Frame>()
190                        .cloned()
191                } else {
192                    None
193                }
194            })
195            .collect()
196    }
197
198    /// Evaluates a JavaScript expression and returns a handle to the result.
199    ///
200    /// Unlike [`evaluate`](Frame::evaluate) which serializes the return value to JSON,
201    /// `evaluate_handle` returns a handle to the in-browser object. This is useful when
202    /// the return value is a non-serializable DOM element or complex JS object.
203    ///
204    /// # Arguments
205    ///
206    /// * `expression` - JavaScript expression to evaluate in the frame context
207    ///
208    /// # Returns
209    ///
210    /// An `Arc<ElementHandle>` pointing to the in-browser object.
211    ///
212    /// # Example
213    ///
214    /// ```ignore
215    /// # use playwright_rs::protocol::Playwright;
216    /// # #[tokio::main]
217    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
218    /// let playwright = Playwright::launch().await?;
219    /// let browser = playwright.chromium().launch().await?;
220    /// let page = browser.new_page().await?;
221    /// page.goto("https://example.com", None).await?;
222    /// let frame = page.main_frame().await?;
223    ///
224    /// let handle = frame.evaluate_handle("document.body").await?;
225    /// let screenshot = handle.screenshot(None).await?;
226    /// # Ok(())
227    /// # }
228    /// ```
229    ///
230    /// # Errors
231    ///
232    /// Returns error if:
233    /// - The JavaScript expression throws an error
234    /// - The result handle GUID cannot be found in the registry
235    /// - Communication with the browser fails
236    ///
237    /// See: <https://playwright.dev/docs/api/class-frame#frame-evaluate-handle>
238    pub async fn evaluate_handle(
239        &self,
240        expression: &str,
241    ) -> Result<Arc<crate::protocol::ElementHandle>> {
242        let params = serde_json::json!({
243            "expression": expression,
244            "isFunction": false,
245            "arg": {"value": {"v": "undefined"}, "handles": []}
246        });
247
248        // The server returns {"handle": {"guid": "JSHandle@..."}}
249        #[derive(Deserialize)]
250        struct HandleRef {
251            guid: String,
252        }
253        #[derive(Deserialize)]
254        struct EvaluateHandleResponse {
255            handle: HandleRef,
256        }
257
258        let response: EvaluateHandleResponse = self
259            .channel()
260            .send("evaluateExpressionHandle", params)
261            .await?;
262
263        let guid = &response.handle.guid;
264
265        // Look up in the connection registry with retry (the __create__ may arrive slightly later)
266        let connection = self.base.connection();
267        let mut attempts = 0;
268        let max_attempts = 20;
269        let handle = loop {
270            match connection
271                .get_typed::<crate::protocol::ElementHandle>(guid)
272                .await
273            {
274                Ok(h) => break h,
275                Err(_) if attempts < max_attempts => {
276                    attempts += 1;
277                    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
278                }
279                Err(e) => return Err(e),
280            }
281        };
282
283        Ok(Arc::new(handle))
284    }
285
286    /// Creates a [`Locator`](crate::protocol::Locator) scoped to this frame.
287    ///
288    /// The locator is lazy — it does not query the DOM until an action is performed on it.
289    ///
290    /// # Arguments
291    ///
292    /// * `selector` - A CSS selector or other Playwright selector strategy
293    ///
294    /// # Panics
295    ///
296    /// Panics if the owning Page has not been set (i.e., `set_page()` was never called).
297    /// In normal usage the main frame always has its page wired up by `Page::main_frame()`.
298    ///
299    /// See: <https://playwright.dev/docs/api/class-frame#frame-locator>
300    pub fn locator(&self, selector: &str) -> crate::protocol::Locator {
301        let page = self
302            .page()
303            .expect("Frame::locator() called before set_page(); call page.main_frame() first");
304        crate::protocol::Locator::new(Arc::new(self.clone()), selector.to_string(), page)
305    }
306
307    /// Returns a locator that matches elements containing the given text.
308    ///
309    /// See: <https://playwright.dev/docs/api/class-frame#frame-get-by-text>
310    pub fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
311        self.locator(&crate::protocol::locator::get_by_text_selector(text, exact))
312    }
313
314    /// Returns a locator that matches elements by their associated label text.
315    ///
316    /// See: <https://playwright.dev/docs/api/class-frame#frame-get-by-label>
317    pub fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
318        self.locator(&crate::protocol::locator::get_by_label_selector(
319            text, exact,
320        ))
321    }
322
323    /// Returns a locator that matches elements by their placeholder text.
324    ///
325    /// See: <https://playwright.dev/docs/api/class-frame#frame-get-by-placeholder>
326    pub fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
327        self.locator(&crate::protocol::locator::get_by_placeholder_selector(
328            text, exact,
329        ))
330    }
331
332    /// Returns a locator that matches elements by their alt text.
333    ///
334    /// See: <https://playwright.dev/docs/api/class-frame#frame-get-by-alt-text>
335    pub fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
336        self.locator(&crate::protocol::locator::get_by_alt_text_selector(
337            text, exact,
338        ))
339    }
340
341    /// Returns a locator that matches elements by their title attribute.
342    ///
343    /// See: <https://playwright.dev/docs/api/class-frame#frame-get-by-title>
344    pub fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
345        self.locator(&crate::protocol::locator::get_by_title_selector(
346            text, exact,
347        ))
348    }
349
350    /// Returns a locator that matches elements by their `data-testid` attribute.
351    ///
352    /// See: <https://playwright.dev/docs/api/class-frame#frame-get-by-test-id>
353    pub fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
354        self.locator(&crate::protocol::locator::get_by_test_id_selector(test_id))
355    }
356
357    /// Returns a locator that matches elements by their ARIA role.
358    ///
359    /// See: <https://playwright.dev/docs/api/class-frame#frame-get-by-role>
360    pub fn get_by_role(
361        &self,
362        role: crate::protocol::locator::AriaRole,
363        options: Option<crate::protocol::locator::GetByRoleOptions>,
364    ) -> crate::protocol::Locator {
365        self.locator(&crate::protocol::locator::get_by_role_selector(
366            role, options,
367        ))
368    }
369
370    /// Returns the channel for sending protocol messages
371    fn channel(&self) -> &Channel {
372        self.base.channel()
373    }
374
375    /// Returns the current URL of the frame.
376    ///
377    /// This returns the last committed URL. Initially, frames are at "about:blank".
378    ///
379    /// See: <https://playwright.dev/docs/api/class-frame#frame-url>
380    pub fn url(&self) -> String {
381        self.url.read().unwrap().clone()
382    }
383
384    /// Navigates the frame to the specified URL.
385    ///
386    /// This is the actual protocol method for navigation. Page.goto() delegates to this.
387    ///
388    /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
389    /// about:blank). This matches Playwright's behavior across all language bindings.
390    ///
391    /// # Arguments
392    ///
393    /// * `url` - The URL to navigate to
394    /// * `options` - Optional navigation options (timeout, wait_until)
395    ///
396    /// See: <https://playwright.dev/docs/api/class-frame#frame-goto>
397    pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
398        // Build params manually using json! macro
399        let mut params = serde_json::json!({
400            "url": url,
401        });
402
403        // Add optional parameters
404        if let Some(opts) = options {
405            if let Some(timeout) = opts.timeout {
406                params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
407            } else {
408                // Default timeout required in Playwright 1.56.1+
409                params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
410            }
411            if let Some(wait_until) = opts.wait_until {
412                params["waitUntil"] = serde_json::json!(wait_until.as_str());
413            }
414        } else {
415            // No options provided, set default timeout (required in Playwright 1.56.1+)
416            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
417        }
418
419        // Send goto RPC to Frame
420        // The server returns { "response": { "guid": "..." } } or null
421        #[derive(Deserialize)]
422        struct GotoResponse {
423            response: Option<ResponseReference>,
424        }
425
426        #[derive(Deserialize)]
427        struct ResponseReference {
428            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
429            guid: Arc<str>,
430        }
431
432        let goto_result: GotoResponse = self.channel().send("goto", params).await?;
433
434        // If navigation returned a response, get the Response object from the connection
435        if let Some(response_ref) = goto_result.response {
436            // The server returns a Response GUID, but the __create__ message might not have
437            // arrived yet. Retry a few times to wait for the object to be created.
438            // TODO(Phase 4+): Implement proper GUID replacement like Python's _replace_guids_with_channels
439            //   - Eliminates retry loop for better performance
440            //   - See: playwright-python's _replace_guids_with_channels method
441            let response_arc = {
442                let mut attempts = 0;
443                let max_attempts = 20; // 20 * 50ms = 1 second max wait
444                loop {
445                    match self.connection().get_object(&response_ref.guid).await {
446                        Ok(obj) => break obj,
447                        Err(_) if attempts < max_attempts => {
448                            attempts += 1;
449                            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
450                        }
451                        Err(e) => return Err(e),
452                    }
453                }
454            };
455
456            // Extract Response data from the initializer, and store the Arc for RPC calls
457            // (body(), rawHeaders(), headerValue()) that need to contact the server.
458            let initializer = response_arc.initializer();
459
460            // Extract response data from initializer
461            let status = initializer["status"].as_u64().ok_or_else(|| {
462                crate::error::Error::ProtocolError("Response missing status".to_string())
463            })? as u16;
464
465            // Convert headers from array format to HashMap
466            let headers = initializer["headers"]
467                .as_array()
468                .ok_or_else(|| {
469                    crate::error::Error::ProtocolError("Response missing headers".to_string())
470                })?
471                .iter()
472                .filter_map(|h| {
473                    let name = h["name"].as_str()?;
474                    let value = h["value"].as_str()?;
475                    Some((name.to_string(), value.to_string()))
476                })
477                .collect();
478
479            Ok(Some(Response::new(
480                initializer["url"]
481                    .as_str()
482                    .ok_or_else(|| {
483                        crate::error::Error::ProtocolError("Response missing url".to_string())
484                    })?
485                    .to_string(),
486                status,
487                initializer["statusText"].as_str().unwrap_or("").to_string(),
488                headers,
489                Some(response_arc),
490            )))
491        } else {
492            // Navigation returned null (e.g., data URLs, about:blank)
493            // This is a valid result, not an error
494            Ok(None)
495        }
496    }
497
498    /// Returns the frame's title.
499    ///
500    /// See: <https://playwright.dev/docs/api/class-frame#frame-title>
501    pub async fn title(&self) -> Result<String> {
502        #[derive(Deserialize)]
503        struct TitleResponse {
504            value: String,
505        }
506
507        let response: TitleResponse = self.channel().send("title", serde_json::json!({})).await?;
508        Ok(response.value)
509    }
510
511    /// Returns the full HTML content of the frame, including the DOCTYPE.
512    ///
513    /// See: <https://playwright.dev/docs/api/class-frame#frame-content>
514    pub async fn content(&self) -> Result<String> {
515        #[derive(Deserialize)]
516        struct ContentResponse {
517            value: String,
518        }
519
520        let response: ContentResponse = self
521            .channel()
522            .send("content", serde_json::json!({}))
523            .await?;
524        Ok(response.value)
525    }
526
527    /// Sets the content of the frame.
528    ///
529    /// See: <https://playwright.dev/docs/api/class-frame#frame-set-content>
530    pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
531        let mut params = serde_json::json!({
532            "html": html,
533        });
534
535        if let Some(opts) = options {
536            if let Some(timeout) = opts.timeout {
537                params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
538            } else {
539                params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
540            }
541            if let Some(wait_until) = opts.wait_until {
542                params["waitUntil"] = serde_json::json!(wait_until.as_str());
543            }
544        } else {
545            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
546        }
547
548        self.channel().send_no_result("setContent", params).await
549    }
550
551    /// Waits for the required load state to be reached.
552    ///
553    /// Playwright's protocol doesn't expose `waitForLoadState` as a server-side command —
554    /// it's implemented client-side using lifecycle events. We implement it by polling
555    /// `document.readyState` via JavaScript evaluation.
556    ///
557    /// See: <https://playwright.dev/docs/api/class-frame#frame-wait-for-load-state>
558    pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
559        let target_state = state.unwrap_or(WaitUntil::Load);
560
561        let js_check = match target_state {
562            // "load" means the full page has loaded (readyState === "complete")
563            WaitUntil::Load => "document.readyState === 'complete'",
564            // "domcontentloaded" means DOM is ready (readyState !== "loading")
565            WaitUntil::DomContentLoaded => "document.readyState !== 'loading'",
566            // "networkidle" has no direct readyState equivalent; we approximate
567            // by checking "complete" (same as Load)
568            WaitUntil::NetworkIdle => "document.readyState === 'complete'",
569            // "commit" means any response has been received (readyState !== "loading" at minimum)
570            WaitUntil::Commit => "document.readyState !== 'loading'",
571        };
572
573        let timeout_ms = crate::DEFAULT_TIMEOUT_MS as u64;
574        let poll_interval = std::time::Duration::from_millis(50);
575        let start = std::time::Instant::now();
576
577        loop {
578            #[derive(Deserialize)]
579            struct EvalResponse {
580                value: serde_json::Value,
581            }
582
583            let result: EvalResponse = self
584                .channel()
585                .send(
586                    "evaluateExpression",
587                    serde_json::json!({
588                        "expression": js_check,
589                        "isFunction": false,
590                        "arg": crate::protocol::serialize_null(),
591                    }),
592                )
593                .await?;
594
595            // Playwright protocol returns booleans as {"b": true/false}
596            let is_ready = result
597                .value
598                .as_object()
599                .and_then(|m| m.get("b"))
600                .and_then(|v| v.as_bool())
601                .unwrap_or(false);
602
603            if is_ready {
604                return Ok(());
605            }
606
607            if start.elapsed().as_millis() as u64 >= timeout_ms {
608                return Err(crate::error::Error::Timeout(format!(
609                    "wait_for_load_state({}) timed out after {}ms",
610                    target_state.as_str(),
611                    timeout_ms
612                )));
613            }
614
615            tokio::time::sleep(poll_interval).await;
616        }
617    }
618
619    /// Waits for the frame to navigate to a URL matching the given string or glob pattern.
620    ///
621    /// Playwright's protocol doesn't expose `waitForURL` as a server-side command —
622    /// it's implemented client-side. We implement it by polling `window.location.href`.
623    ///
624    /// See: <https://playwright.dev/docs/api/class-frame#frame-wait-for-url>
625    pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
626        let timeout_ms = options
627            .as_ref()
628            .and_then(|o| o.timeout)
629            .map(|d| d.as_millis() as u64)
630            .unwrap_or(crate::DEFAULT_TIMEOUT_MS as u64);
631
632        // Convert glob pattern to regex for matching
633        // Playwright supports string (exact), glob (**), and regex patterns
634        // We support exact string and basic glob patterns
635        let is_glob = url.contains('*');
636
637        let poll_interval = std::time::Duration::from_millis(50);
638        let start = std::time::Instant::now();
639
640        loop {
641            let current_url = self.url();
642
643            let matches = if is_glob {
644                glob_match(url, &current_url)
645            } else {
646                current_url == url
647            };
648
649            if matches {
650                // URL matches — optionally wait for load state
651                if let Some(ref opts) = options
652                    && let Some(wait_until) = opts.wait_until
653                {
654                    self.wait_for_load_state(Some(wait_until)).await?;
655                }
656                return Ok(());
657            }
658
659            if start.elapsed().as_millis() as u64 >= timeout_ms {
660                return Err(crate::error::Error::Timeout(format!(
661                    "wait_for_url({}) timed out after {}ms, current URL: {}",
662                    url, timeout_ms, current_url
663                )));
664            }
665
666            tokio::time::sleep(poll_interval).await;
667        }
668    }
669
670    /// Returns the first element matching the selector, or None if not found.
671    ///
672    /// See: <https://playwright.dev/docs/api/class-frame#frame-query-selector>
673    pub async fn query_selector(
674        &self,
675        selector: &str,
676    ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
677        let response: serde_json::Value = self
678            .channel()
679            .send(
680                "querySelector",
681                serde_json::json!({
682                    "selector": selector
683                }),
684            )
685            .await?;
686
687        // Check if response is empty (no element found)
688        if response.as_object().map(|o| o.is_empty()).unwrap_or(true) {
689            return Ok(None);
690        }
691
692        // Try different possible field names
693        let element_value = if let Some(elem) = response.get("element") {
694            elem
695        } else if let Some(elem) = response.get("handle") {
696            elem
697        } else {
698            // Maybe the response IS the guid object itself
699            &response
700        };
701
702        if element_value.is_null() {
703            return Ok(None);
704        }
705
706        // Element response contains { guid: "elementHandle@123" }
707        let guid = element_value["guid"].as_str().ok_or_else(|| {
708            crate::error::Error::ProtocolError("Element GUID missing".to_string())
709        })?;
710
711        // Look up the ElementHandle object in the connection's object registry and downcast
712        let connection = self.base.connection();
713        let handle: crate::protocol::ElementHandle = connection
714            .get_typed::<crate::protocol::ElementHandle>(guid)
715            .await?;
716
717        Ok(Some(Arc::new(handle)))
718    }
719
720    /// Returns all elements matching the selector.
721    ///
722    /// See: <https://playwright.dev/docs/api/class-frame#frame-query-selector-all>
723    pub async fn query_selector_all(
724        &self,
725        selector: &str,
726    ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
727        #[derive(Deserialize)]
728        struct QueryAllResponse {
729            elements: Vec<serde_json::Value>,
730        }
731
732        let response: QueryAllResponse = self
733            .channel()
734            .send(
735                "querySelectorAll",
736                serde_json::json!({
737                    "selector": selector
738                }),
739            )
740            .await?;
741
742        // Convert GUID responses to ElementHandle objects
743        let connection = self.base.connection();
744        let mut handles = Vec::new();
745
746        for element_value in response.elements {
747            let guid = element_value["guid"].as_str().ok_or_else(|| {
748                crate::error::Error::ProtocolError("Element GUID missing".to_string())
749            })?;
750
751            let handle: crate::protocol::ElementHandle = connection
752                .get_typed::<crate::protocol::ElementHandle>(guid)
753                .await?;
754
755            handles.push(Arc::new(handle));
756        }
757
758        Ok(handles)
759    }
760
761    // Locator delegate methods
762    // These are called by Locator to perform actual queries
763
764    /// Returns the number of elements matching the selector.
765    pub(crate) async fn locator_count(&self, selector: &str) -> Result<usize> {
766        // Use querySelectorAll which returns array of element handles
767        #[derive(Deserialize)]
768        struct QueryAllResponse {
769            elements: Vec<serde_json::Value>,
770        }
771
772        let response: QueryAllResponse = self
773            .channel()
774            .send(
775                "querySelectorAll",
776                serde_json::json!({
777                    "selector": selector
778                }),
779            )
780            .await?;
781
782        Ok(response.elements.len())
783    }
784
785    /// Returns the text content of the element.
786    pub(crate) async fn locator_text_content(&self, selector: &str) -> Result<Option<String>> {
787        #[derive(Deserialize)]
788        struct TextContentResponse {
789            value: Option<String>,
790        }
791
792        let response: TextContentResponse = self
793            .channel()
794            .send(
795                "textContent",
796                serde_json::json!({
797                    "selector": selector,
798                    "strict": true,
799                    "timeout": crate::DEFAULT_TIMEOUT_MS
800                }),
801            )
802            .await?;
803
804        Ok(response.value)
805    }
806
807    /// Returns the inner text of the element.
808    pub(crate) async fn locator_inner_text(&self, selector: &str) -> Result<String> {
809        #[derive(Deserialize)]
810        struct InnerTextResponse {
811            value: String,
812        }
813
814        let response: InnerTextResponse = self
815            .channel()
816            .send(
817                "innerText",
818                serde_json::json!({
819                    "selector": selector,
820                    "strict": true,
821                    "timeout": crate::DEFAULT_TIMEOUT_MS
822                }),
823            )
824            .await?;
825
826        Ok(response.value)
827    }
828
829    /// Returns the inner HTML of the element.
830    pub(crate) async fn locator_inner_html(&self, selector: &str) -> Result<String> {
831        #[derive(Deserialize)]
832        struct InnerHTMLResponse {
833            value: String,
834        }
835
836        let response: InnerHTMLResponse = self
837            .channel()
838            .send(
839                "innerHTML",
840                serde_json::json!({
841                    "selector": selector,
842                    "strict": true,
843                    "timeout": crate::DEFAULT_TIMEOUT_MS
844                }),
845            )
846            .await?;
847
848        Ok(response.value)
849    }
850
851    /// Returns the value of the specified attribute.
852    pub(crate) async fn locator_get_attribute(
853        &self,
854        selector: &str,
855        name: &str,
856    ) -> Result<Option<String>> {
857        #[derive(Deserialize)]
858        struct GetAttributeResponse {
859            value: Option<String>,
860        }
861
862        let response: GetAttributeResponse = self
863            .channel()
864            .send(
865                "getAttribute",
866                serde_json::json!({
867                    "selector": selector,
868                    "name": name,
869                    "strict": true,
870                    "timeout": crate::DEFAULT_TIMEOUT_MS
871                }),
872            )
873            .await?;
874
875        Ok(response.value)
876    }
877
878    /// Returns whether the element is visible.
879    pub(crate) async fn locator_is_visible(&self, selector: &str) -> Result<bool> {
880        #[derive(Deserialize)]
881        struct IsVisibleResponse {
882            value: bool,
883        }
884
885        let response: IsVisibleResponse = self
886            .channel()
887            .send(
888                "isVisible",
889                serde_json::json!({
890                    "selector": selector,
891                    "strict": true,
892                    "timeout": crate::DEFAULT_TIMEOUT_MS
893                }),
894            )
895            .await?;
896
897        Ok(response.value)
898    }
899
900    /// Returns whether the element is enabled.
901    pub(crate) async fn locator_is_enabled(&self, selector: &str) -> Result<bool> {
902        #[derive(Deserialize)]
903        struct IsEnabledResponse {
904            value: bool,
905        }
906
907        let response: IsEnabledResponse = self
908            .channel()
909            .send(
910                "isEnabled",
911                serde_json::json!({
912                    "selector": selector,
913                    "strict": true,
914                    "timeout": crate::DEFAULT_TIMEOUT_MS
915                }),
916            )
917            .await?;
918
919        Ok(response.value)
920    }
921
922    /// Returns whether the checkbox or radio button is checked.
923    pub(crate) async fn locator_is_checked(&self, selector: &str) -> Result<bool> {
924        #[derive(Deserialize)]
925        struct IsCheckedResponse {
926            value: bool,
927        }
928
929        let response: IsCheckedResponse = self
930            .channel()
931            .send(
932                "isChecked",
933                serde_json::json!({
934                    "selector": selector,
935                    "strict": true,
936                    "timeout": crate::DEFAULT_TIMEOUT_MS
937                }),
938            )
939            .await?;
940
941        Ok(response.value)
942    }
943
944    /// Returns whether the element is editable.
945    pub(crate) async fn locator_is_editable(&self, selector: &str) -> Result<bool> {
946        #[derive(Deserialize)]
947        struct IsEditableResponse {
948            value: bool,
949        }
950
951        let response: IsEditableResponse = self
952            .channel()
953            .send(
954                "isEditable",
955                serde_json::json!({
956                    "selector": selector,
957                    "strict": true,
958                    "timeout": crate::DEFAULT_TIMEOUT_MS
959                }),
960            )
961            .await?;
962
963        Ok(response.value)
964    }
965
966    /// Returns whether the element is hidden.
967    pub(crate) async fn locator_is_hidden(&self, selector: &str) -> Result<bool> {
968        #[derive(Deserialize)]
969        struct IsHiddenResponse {
970            value: bool,
971        }
972
973        let response: IsHiddenResponse = self
974            .channel()
975            .send(
976                "isHidden",
977                serde_json::json!({
978                    "selector": selector,
979                    "strict": true,
980                    "timeout": crate::DEFAULT_TIMEOUT_MS
981                }),
982            )
983            .await?;
984
985        Ok(response.value)
986    }
987
988    /// Returns whether the element is disabled.
989    pub(crate) async fn locator_is_disabled(&self, selector: &str) -> Result<bool> {
990        #[derive(Deserialize)]
991        struct IsDisabledResponse {
992            value: bool,
993        }
994
995        let response: IsDisabledResponse = self
996            .channel()
997            .send(
998                "isDisabled",
999                serde_json::json!({
1000                    "selector": selector,
1001                    "strict": true,
1002                    "timeout": crate::DEFAULT_TIMEOUT_MS
1003                }),
1004            )
1005            .await?;
1006
1007        Ok(response.value)
1008    }
1009
1010    /// Returns whether the element is focused (currently has focus).
1011    ///
1012    /// This implementation checks if the element is the activeElement in the DOM
1013    /// using JavaScript evaluation, since Playwright doesn't expose isFocused() at
1014    /// the protocol level.
1015    pub(crate) async fn locator_is_focused(&self, selector: &str) -> Result<bool> {
1016        #[derive(Deserialize)]
1017        struct EvaluateResult {
1018            value: serde_json::Value,
1019        }
1020
1021        // Use JavaScript to check if the element is the active element
1022        // The script queries the DOM and returns true/false
1023        let script = r#"selector => {
1024                const elements = document.querySelectorAll(selector);
1025                if (elements.length === 0) return false;
1026                const element = elements[0];
1027                return document.activeElement === element;
1028            }"#;
1029
1030        let params = serde_json::json!({
1031            "expression": script,
1032            "arg": {
1033                "value": {"s": selector},
1034                "handles": []
1035            }
1036        });
1037
1038        let result: EvaluateResult = self.channel().send("evaluateExpression", params).await?;
1039
1040        // Playwright protocol returns booleans as {"b": true} or {"b": false}
1041        if let serde_json::Value::Object(map) = &result.value
1042            && let Some(b) = map.get("b").and_then(|v| v.as_bool())
1043        {
1044            return Ok(b);
1045        }
1046
1047        // Fallback: check if the string representation is "true"
1048        Ok(result.value.to_string().to_lowercase().contains("true"))
1049    }
1050
1051    // Action delegate methods
1052
1053    /// Clicks the element matching the selector.
1054    pub(crate) async fn locator_click(
1055        &self,
1056        selector: &str,
1057        options: Option<crate::protocol::ClickOptions>,
1058    ) -> Result<()> {
1059        let mut params = serde_json::json!({
1060            "selector": selector,
1061            "strict": true
1062        });
1063
1064        if let Some(opts) = options {
1065            let opts_json = opts.to_json();
1066            if let Some(obj) = params.as_object_mut()
1067                && let Some(opts_obj) = opts_json.as_object()
1068            {
1069                obj.extend(opts_obj.clone());
1070            }
1071        } else {
1072            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1073        }
1074
1075        self.channel()
1076            .send_no_result("click", params)
1077            .await
1078            .map_err(|e| match e {
1079                Error::Timeout(msg) => {
1080                    Error::Timeout(format!("{} (selector: '{}')", msg, selector))
1081                }
1082                other => other,
1083            })
1084    }
1085
1086    /// Double clicks the element matching the selector.
1087    pub(crate) async fn locator_dblclick(
1088        &self,
1089        selector: &str,
1090        options: Option<crate::protocol::ClickOptions>,
1091    ) -> Result<()> {
1092        let mut params = serde_json::json!({
1093            "selector": selector,
1094            "strict": true
1095        });
1096
1097        if let Some(opts) = options {
1098            let opts_json = opts.to_json();
1099            if let Some(obj) = params.as_object_mut()
1100                && let Some(opts_obj) = opts_json.as_object()
1101            {
1102                obj.extend(opts_obj.clone());
1103            }
1104        } else {
1105            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1106        }
1107
1108        self.channel().send_no_result("dblclick", params).await
1109    }
1110
1111    /// Fills the element with text.
1112    pub(crate) async fn locator_fill(
1113        &self,
1114        selector: &str,
1115        text: &str,
1116        options: Option<crate::protocol::FillOptions>,
1117    ) -> Result<()> {
1118        let mut params = serde_json::json!({
1119            "selector": selector,
1120            "value": text,
1121            "strict": true
1122        });
1123
1124        if let Some(opts) = options {
1125            let opts_json = opts.to_json();
1126            if let Some(obj) = params.as_object_mut()
1127                && let Some(opts_obj) = opts_json.as_object()
1128            {
1129                obj.extend(opts_obj.clone());
1130            }
1131        } else {
1132            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1133        }
1134
1135        self.channel().send_no_result("fill", params).await
1136    }
1137
1138    /// Clears the element's value.
1139    pub(crate) async fn locator_clear(
1140        &self,
1141        selector: &str,
1142        options: Option<crate::protocol::FillOptions>,
1143    ) -> Result<()> {
1144        let mut params = serde_json::json!({
1145            "selector": selector,
1146            "value": "",
1147            "strict": true
1148        });
1149
1150        if let Some(opts) = options {
1151            let opts_json = opts.to_json();
1152            if let Some(obj) = params.as_object_mut()
1153                && let Some(opts_obj) = opts_json.as_object()
1154            {
1155                obj.extend(opts_obj.clone());
1156            }
1157        } else {
1158            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1159        }
1160
1161        self.channel().send_no_result("fill", params).await
1162    }
1163
1164    /// Presses a key on the element.
1165    pub(crate) async fn locator_press(
1166        &self,
1167        selector: &str,
1168        key: &str,
1169        options: Option<crate::protocol::PressOptions>,
1170    ) -> Result<()> {
1171        let mut params = serde_json::json!({
1172            "selector": selector,
1173            "key": key,
1174            "strict": true
1175        });
1176
1177        if let Some(opts) = options {
1178            let opts_json = opts.to_json();
1179            if let Some(obj) = params.as_object_mut()
1180                && let Some(opts_obj) = opts_json.as_object()
1181            {
1182                obj.extend(opts_obj.clone());
1183            }
1184        } else {
1185            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1186        }
1187
1188        self.channel().send_no_result("press", params).await
1189    }
1190
1191    /// Sets focus on the element matching the selector.
1192    pub(crate) async fn locator_focus(&self, selector: &str) -> Result<()> {
1193        self.channel()
1194            .send_no_result(
1195                "focus",
1196                serde_json::json!({
1197                    "selector": selector,
1198                    "strict": true,
1199                    "timeout": crate::DEFAULT_TIMEOUT_MS
1200                }),
1201            )
1202            .await
1203    }
1204
1205    /// Removes focus from the element matching the selector.
1206    pub(crate) async fn locator_blur(&self, selector: &str) -> Result<()> {
1207        self.channel()
1208            .send_no_result(
1209                "blur",
1210                serde_json::json!({
1211                    "selector": selector,
1212                    "strict": true,
1213                    "timeout": crate::DEFAULT_TIMEOUT_MS
1214                }),
1215            )
1216            .await
1217    }
1218
1219    /// Types text into the element character by character.
1220    ///
1221    /// Uses the Playwright protocol `"type"` message (the legacy name for pressSequentially).
1222    pub(crate) async fn locator_press_sequentially(
1223        &self,
1224        selector: &str,
1225        text: &str,
1226        options: Option<crate::protocol::PressSequentiallyOptions>,
1227    ) -> Result<()> {
1228        let mut params = serde_json::json!({
1229            "selector": selector,
1230            "text": text,
1231            "strict": true
1232        });
1233
1234        if let Some(opts) = options {
1235            let opts_json = opts.to_json();
1236            if let Some(obj) = params.as_object_mut()
1237                && let Some(opts_obj) = opts_json.as_object()
1238            {
1239                obj.extend(opts_obj.clone());
1240            }
1241        } else {
1242            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1243        }
1244
1245        self.channel().send_no_result("type", params).await
1246    }
1247
1248    /// Returns the inner text of all elements matching the selector.
1249    pub(crate) async fn locator_all_inner_texts(&self, selector: &str) -> Result<Vec<String>> {
1250        #[derive(serde::Deserialize)]
1251        struct EvaluateResult {
1252            value: serde_json::Value,
1253        }
1254
1255        // The Playwright protocol's evalOnSelectorAll requires an `arg` field.
1256        // We pass a null argument since our expression doesn't use one.
1257        let params = serde_json::json!({
1258            "selector": selector,
1259            "expression": "ee => ee.map(e => e.innerText)",
1260            "isFunction": true,
1261            "arg": {
1262                "value": {"v": "null"},
1263                "handles": []
1264            }
1265        });
1266
1267        let result: EvaluateResult = self.channel().send("evalOnSelectorAll", params).await?;
1268
1269        Self::parse_string_array(result.value)
1270    }
1271
1272    /// Returns the text content of all elements matching the selector.
1273    pub(crate) async fn locator_all_text_contents(&self, selector: &str) -> Result<Vec<String>> {
1274        #[derive(serde::Deserialize)]
1275        struct EvaluateResult {
1276            value: serde_json::Value,
1277        }
1278
1279        // The Playwright protocol's evalOnSelectorAll requires an `arg` field.
1280        // We pass a null argument since our expression doesn't use one.
1281        let params = serde_json::json!({
1282            "selector": selector,
1283            "expression": "ee => ee.map(e => e.textContent || '')",
1284            "isFunction": true,
1285            "arg": {
1286                "value": {"v": "null"},
1287                "handles": []
1288            }
1289        });
1290
1291        let result: EvaluateResult = self.channel().send("evalOnSelectorAll", params).await?;
1292
1293        Self::parse_string_array(result.value)
1294    }
1295
1296    /// Performs a touch-tap on the element matching the selector.
1297    ///
1298    /// Sends touch events rather than mouse events. Requires the browser context to be
1299    /// created with `has_touch: true`.
1300    ///
1301    /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1302    pub(crate) async fn locator_tap(
1303        &self,
1304        selector: &str,
1305        options: Option<crate::protocol::TapOptions>,
1306    ) -> Result<()> {
1307        let mut params = serde_json::json!({
1308            "selector": selector,
1309            "strict": true
1310        });
1311
1312        if let Some(opts) = options {
1313            let opts_json = opts.to_json();
1314            if let Some(obj) = params.as_object_mut()
1315                && let Some(opts_obj) = opts_json.as_object()
1316            {
1317                obj.extend(opts_obj.clone());
1318            }
1319        } else {
1320            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1321        }
1322
1323        self.channel().send_no_result("tap", params).await
1324    }
1325
1326    /// Drags the source element onto the target element.
1327    ///
1328    /// Both selectors must resolve to elements in this frame.
1329    ///
1330    /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1331    pub(crate) async fn locator_drag_to(
1332        &self,
1333        source_selector: &str,
1334        target_selector: &str,
1335        options: Option<crate::protocol::DragToOptions>,
1336    ) -> Result<()> {
1337        let mut params = serde_json::json!({
1338            "source": source_selector,
1339            "target": target_selector,
1340            "strict": true
1341        });
1342
1343        if let Some(opts) = options {
1344            let opts_json = opts.to_json();
1345            if let Some(obj) = params.as_object_mut()
1346                && let Some(opts_obj) = opts_json.as_object()
1347            {
1348                obj.extend(opts_obj.clone());
1349            }
1350        } else {
1351            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1352        }
1353
1354        self.channel().send_no_result("dragAndDrop", params).await
1355    }
1356
1357    /// Waits for the element to satisfy a state condition.
1358    ///
1359    /// Uses Playwright's `waitForSelector` RPC. The element state defaults to `visible`
1360    /// if not specified.
1361    ///
1362    /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1363    pub(crate) async fn locator_wait_for(
1364        &self,
1365        selector: &str,
1366        options: Option<crate::protocol::WaitForOptions>,
1367    ) -> Result<()> {
1368        let mut params = serde_json::json!({
1369            "selector": selector,
1370            "strict": true
1371        });
1372
1373        if let Some(opts) = options {
1374            let opts_json = opts.to_json();
1375            if let Some(obj) = params.as_object_mut()
1376                && let Some(opts_obj) = opts_json.as_object()
1377            {
1378                obj.extend(opts_obj.clone());
1379            }
1380        } else {
1381            // Default: wait for visible with default timeout
1382            params["state"] = serde_json::json!("visible");
1383            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1384        }
1385
1386        // waitForSelector returns an ElementHandle or null — we discard the return value
1387        let _: serde_json::Value = self.channel().send("waitForSelector", params).await?;
1388        Ok(())
1389    }
1390
1391    /// Evaluates a JavaScript expression in the scope of the element matching the selector.
1392    ///
1393    /// The element is passed as the first argument to the expression. This is equivalent
1394    /// to Playwright's `evalOnSelector` protocol call with `strict: true`.
1395    ///
1396    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1397    pub(crate) async fn locator_evaluate<T: serde::Serialize>(
1398        &self,
1399        selector: &str,
1400        expression: &str,
1401        arg: Option<T>,
1402    ) -> Result<serde_json::Value> {
1403        let serialized_arg = match arg {
1404            Some(a) => serialize_argument(&a),
1405            None => serialize_null(),
1406        };
1407
1408        let params = serde_json::json!({
1409            "selector": selector,
1410            "expression": expression,
1411            "isFunction": true,
1412            "arg": serialized_arg,
1413            "strict": true
1414        });
1415
1416        #[derive(Deserialize)]
1417        struct EvaluateResult {
1418            value: serde_json::Value,
1419        }
1420
1421        let result: EvaluateResult = self.channel().send("evalOnSelector", params).await?;
1422        Ok(parse_result(&result.value))
1423    }
1424
1425    /// Evaluates a JavaScript expression in the scope of all elements matching the selector.
1426    ///
1427    /// The array of all matching elements is passed as the first argument to the expression.
1428    /// This is equivalent to Playwright's `evalOnSelectorAll` protocol call.
1429    ///
1430    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1431    pub(crate) async fn locator_evaluate_all<T: serde::Serialize>(
1432        &self,
1433        selector: &str,
1434        expression: &str,
1435        arg: Option<T>,
1436    ) -> Result<serde_json::Value> {
1437        let serialized_arg = match arg {
1438            Some(a) => serialize_argument(&a),
1439            None => serialize_null(),
1440        };
1441
1442        let params = serde_json::json!({
1443            "selector": selector,
1444            "expression": expression,
1445            "isFunction": true,
1446            "arg": serialized_arg
1447        });
1448
1449        #[derive(Deserialize)]
1450        struct EvaluateResult {
1451            value: serde_json::Value,
1452        }
1453
1454        let result: EvaluateResult = self.channel().send("evalOnSelectorAll", params).await?;
1455        Ok(parse_result(&result.value))
1456    }
1457
1458    /// Parses a Playwright protocol array value into a Vec<String>.
1459    ///
1460    /// The Playwright protocol returns arrays as:
1461    /// `{"a": [{"s": "value1"}, {"s": "value2"}, ...]}`
1462    fn parse_string_array(value: serde_json::Value) -> Result<Vec<String>> {
1463        // Playwright protocol wraps arrays in {"a": [...]}
1464        let array = if let Some(arr) = value.get("a").and_then(|v| v.as_array()) {
1465            arr.clone()
1466        } else if let Some(arr) = value.as_array() {
1467            arr.clone()
1468        } else {
1469            return Ok(Vec::new());
1470        };
1471
1472        let mut result = Vec::with_capacity(array.len());
1473        for item in &array {
1474            // Each string item is wrapped as {"s": "value"} in Playwright protocol
1475            let s = if let Some(s) = item.get("s").and_then(|v| v.as_str()) {
1476                s.to_string()
1477            } else if let Some(s) = item.as_str() {
1478                s.to_string()
1479            } else if item.is_null() {
1480                String::new()
1481            } else {
1482                item.to_string()
1483            };
1484            result.push(s);
1485        }
1486        Ok(result)
1487    }
1488
1489    pub(crate) async fn locator_check(
1490        &self,
1491        selector: &str,
1492        options: Option<crate::protocol::CheckOptions>,
1493    ) -> Result<()> {
1494        let mut params = serde_json::json!({
1495            "selector": selector,
1496            "strict": true
1497        });
1498
1499        if let Some(opts) = options {
1500            let opts_json = opts.to_json();
1501            if let Some(obj) = params.as_object_mut()
1502                && let Some(opts_obj) = opts_json.as_object()
1503            {
1504                obj.extend(opts_obj.clone());
1505            }
1506        } else {
1507            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1508        }
1509
1510        self.channel().send_no_result("check", params).await
1511    }
1512
1513    pub(crate) async fn locator_uncheck(
1514        &self,
1515        selector: &str,
1516        options: Option<crate::protocol::CheckOptions>,
1517    ) -> Result<()> {
1518        let mut params = serde_json::json!({
1519            "selector": selector,
1520            "strict": true
1521        });
1522
1523        if let Some(opts) = options {
1524            let opts_json = opts.to_json();
1525            if let Some(obj) = params.as_object_mut()
1526                && let Some(opts_obj) = opts_json.as_object()
1527            {
1528                obj.extend(opts_obj.clone());
1529            }
1530        } else {
1531            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1532        }
1533
1534        self.channel().send_no_result("uncheck", params).await
1535    }
1536
1537    pub(crate) async fn locator_hover(
1538        &self,
1539        selector: &str,
1540        options: Option<crate::protocol::HoverOptions>,
1541    ) -> Result<()> {
1542        let mut params = serde_json::json!({
1543            "selector": selector,
1544            "strict": true
1545        });
1546
1547        if let Some(opts) = options {
1548            let opts_json = opts.to_json();
1549            if let Some(obj) = params.as_object_mut()
1550                && let Some(opts_obj) = opts_json.as_object()
1551            {
1552                obj.extend(opts_obj.clone());
1553            }
1554        } else {
1555            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1556        }
1557
1558        self.channel().send_no_result("hover", params).await
1559    }
1560
1561    pub(crate) async fn locator_input_value(&self, selector: &str) -> Result<String> {
1562        #[derive(Deserialize)]
1563        struct InputValueResponse {
1564            value: String,
1565        }
1566
1567        let response: InputValueResponse = self
1568            .channel()
1569            .send(
1570                "inputValue",
1571                serde_json::json!({
1572                    "selector": selector,
1573                    "strict": true,
1574                    "timeout": crate::DEFAULT_TIMEOUT_MS  // Required in Playwright 1.56.1+
1575                }),
1576            )
1577            .await?;
1578
1579        Ok(response.value)
1580    }
1581
1582    pub(crate) async fn locator_select_option(
1583        &self,
1584        selector: &str,
1585        value: crate::protocol::SelectOption,
1586        options: Option<crate::protocol::SelectOptions>,
1587    ) -> Result<Vec<String>> {
1588        #[derive(Deserialize)]
1589        struct SelectOptionResponse {
1590            values: Vec<String>,
1591        }
1592
1593        let mut params = serde_json::json!({
1594            "selector": selector,
1595            "strict": true,
1596            "options": [value.to_json()]
1597        });
1598
1599        if let Some(opts) = options {
1600            let opts_json = opts.to_json();
1601            if let Some(obj) = params.as_object_mut()
1602                && let Some(opts_obj) = opts_json.as_object()
1603            {
1604                obj.extend(opts_obj.clone());
1605            }
1606        } else {
1607            // No options provided, add default timeout (required in Playwright 1.56.1+)
1608            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1609        }
1610
1611        let response: SelectOptionResponse = self.channel().send("selectOption", params).await?;
1612
1613        Ok(response.values)
1614    }
1615
1616    pub(crate) async fn locator_select_option_multiple(
1617        &self,
1618        selector: &str,
1619        values: Vec<crate::protocol::SelectOption>,
1620        options: Option<crate::protocol::SelectOptions>,
1621    ) -> Result<Vec<String>> {
1622        #[derive(Deserialize)]
1623        struct SelectOptionResponse {
1624            values: Vec<String>,
1625        }
1626
1627        let values_array: Vec<_> = values.iter().map(|v| v.to_json()).collect();
1628
1629        let mut params = serde_json::json!({
1630            "selector": selector,
1631            "strict": true,
1632            "options": values_array
1633        });
1634
1635        if let Some(opts) = options {
1636            let opts_json = opts.to_json();
1637            if let Some(obj) = params.as_object_mut()
1638                && let Some(opts_obj) = opts_json.as_object()
1639            {
1640                obj.extend(opts_obj.clone());
1641            }
1642        } else {
1643            // No options provided, add default timeout (required in Playwright 1.56.1+)
1644            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1645        }
1646
1647        let response: SelectOptionResponse = self.channel().send("selectOption", params).await?;
1648
1649        Ok(response.values)
1650    }
1651
1652    pub(crate) async fn locator_set_input_files(
1653        &self,
1654        selector: &str,
1655        file: &std::path::PathBuf,
1656    ) -> Result<()> {
1657        use base64::{Engine as _, engine::general_purpose};
1658        use std::io::Read;
1659
1660        // Read file contents
1661        let mut file_handle = std::fs::File::open(file)?;
1662        let mut buffer = Vec::new();
1663        file_handle.read_to_end(&mut buffer)?;
1664
1665        // Base64 encode the file contents
1666        let base64_content = general_purpose::STANDARD.encode(&buffer);
1667
1668        // Get file name
1669        let file_name = file
1670            .file_name()
1671            .and_then(|n| n.to_str())
1672            .ok_or_else(|| crate::error::Error::InvalidArgument("Invalid file path".to_string()))?;
1673
1674        self.channel()
1675            .send_no_result(
1676                "setInputFiles",
1677                serde_json::json!({
1678                    "selector": selector,
1679                    "strict": true,
1680                    "timeout": crate::DEFAULT_TIMEOUT_MS,  // Required in Playwright 1.56.1+
1681                    "payloads": [{
1682                        "name": file_name,
1683                        "buffer": base64_content
1684                    }]
1685                }),
1686            )
1687            .await
1688    }
1689
1690    pub(crate) async fn locator_set_input_files_multiple(
1691        &self,
1692        selector: &str,
1693        files: &[&std::path::PathBuf],
1694    ) -> Result<()> {
1695        use base64::{Engine as _, engine::general_purpose};
1696        use std::io::Read;
1697
1698        // If empty array, clear the files
1699        if files.is_empty() {
1700            return self
1701                .channel()
1702                .send_no_result(
1703                    "setInputFiles",
1704                    serde_json::json!({
1705                        "selector": selector,
1706                        "strict": true,
1707                        "timeout": crate::DEFAULT_TIMEOUT_MS,  // Required in Playwright 1.56.1+
1708                        "payloads": []
1709                    }),
1710                )
1711                .await;
1712        }
1713
1714        // Read and encode each file
1715        let mut file_objects = Vec::new();
1716        for file_path in files {
1717            let mut file_handle = std::fs::File::open(file_path)?;
1718            let mut buffer = Vec::new();
1719            file_handle.read_to_end(&mut buffer)?;
1720
1721            let base64_content = general_purpose::STANDARD.encode(&buffer);
1722            let file_name = file_path
1723                .file_name()
1724                .and_then(|n| n.to_str())
1725                .ok_or_else(|| {
1726                    crate::error::Error::InvalidArgument("Invalid file path".to_string())
1727                })?;
1728
1729            file_objects.push(serde_json::json!({
1730                "name": file_name,
1731                "buffer": base64_content
1732            }));
1733        }
1734
1735        self.channel()
1736            .send_no_result(
1737                "setInputFiles",
1738                serde_json::json!({
1739                    "selector": selector,
1740                    "strict": true,
1741                    "timeout": crate::DEFAULT_TIMEOUT_MS,  // Required in Playwright 1.56.1+
1742                    "payloads": file_objects
1743                }),
1744            )
1745            .await
1746    }
1747
1748    pub(crate) async fn locator_set_input_files_payload(
1749        &self,
1750        selector: &str,
1751        file: crate::protocol::FilePayload,
1752    ) -> Result<()> {
1753        use base64::{Engine as _, engine::general_purpose};
1754
1755        // Base64 encode the file contents
1756        let base64_content = general_purpose::STANDARD.encode(&file.buffer);
1757
1758        self.channel()
1759            .send_no_result(
1760                "setInputFiles",
1761                serde_json::json!({
1762                    "selector": selector,
1763                    "strict": true,
1764                    "timeout": crate::DEFAULT_TIMEOUT_MS,
1765                    "payloads": [{
1766                        "name": file.name,
1767                        "mimeType": file.mime_type,
1768                        "buffer": base64_content
1769                    }]
1770                }),
1771            )
1772            .await
1773    }
1774
1775    pub(crate) async fn locator_set_input_files_payload_multiple(
1776        &self,
1777        selector: &str,
1778        files: &[crate::protocol::FilePayload],
1779    ) -> Result<()> {
1780        use base64::{Engine as _, engine::general_purpose};
1781
1782        // If empty array, clear the files
1783        if files.is_empty() {
1784            return self
1785                .channel()
1786                .send_no_result(
1787                    "setInputFiles",
1788                    serde_json::json!({
1789                        "selector": selector,
1790                        "strict": true,
1791                        "timeout": crate::DEFAULT_TIMEOUT_MS,
1792                        "payloads": []
1793                    }),
1794                )
1795                .await;
1796        }
1797
1798        // Encode each file
1799        let file_objects: Vec<_> = files
1800            .iter()
1801            .map(|file| {
1802                let base64_content = general_purpose::STANDARD.encode(&file.buffer);
1803                serde_json::json!({
1804                    "name": file.name,
1805                    "mimeType": file.mime_type,
1806                    "buffer": base64_content
1807                })
1808            })
1809            .collect();
1810
1811        self.channel()
1812            .send_no_result(
1813                "setInputFiles",
1814                serde_json::json!({
1815                    "selector": selector,
1816                    "strict": true,
1817                    "timeout": crate::DEFAULT_TIMEOUT_MS,
1818                    "payloads": file_objects
1819                }),
1820            )
1821            .await
1822    }
1823
1824    /// Evaluates JavaScript expression in the frame context (without return value).
1825    ///
1826    /// This is used internally by Page.evaluate().
1827    pub(crate) async fn frame_evaluate_expression(&self, expression: &str) -> Result<()> {
1828        let params = serde_json::json!({
1829            "expression": expression,
1830            "arg": {
1831                "value": {"v": "null"},
1832                "handles": []
1833            }
1834        });
1835
1836        let _: serde_json::Value = self.channel().send("evaluateExpression", params).await?;
1837        Ok(())
1838    }
1839
1840    /// Evaluates JavaScript expression and returns the result as a String.
1841    ///
1842    /// The return value is automatically converted to a string representation.
1843    ///
1844    /// # Arguments
1845    ///
1846    /// * `expression` - JavaScript code to evaluate
1847    ///
1848    /// # Returns
1849    ///
1850    /// The result as a String
1851    pub(crate) async fn frame_evaluate_expression_value(&self, expression: &str) -> Result<String> {
1852        let params = serde_json::json!({
1853            "expression": expression,
1854            "arg": {
1855                "value": {"v": "null"},
1856                "handles": []
1857            }
1858        });
1859
1860        #[derive(Deserialize)]
1861        struct EvaluateResult {
1862            value: serde_json::Value,
1863        }
1864
1865        let result: EvaluateResult = self.channel().send("evaluateExpression", params).await?;
1866
1867        // Playwright protocol returns values in a wrapped format:
1868        // - String: {"s": "value"}
1869        // - Number: {"n": 123}
1870        // - Boolean: {"b": true}
1871        // - Null: {"v": "null"}
1872        // - Undefined: {"v": "undefined"}
1873        match &result.value {
1874            Value::Object(map) => {
1875                if let Some(s) = map.get("s").and_then(|v| v.as_str()) {
1876                    // String value
1877                    Ok(s.to_string())
1878                } else if let Some(n) = map.get("n") {
1879                    // Number value
1880                    Ok(n.to_string())
1881                } else if let Some(b) = map.get("b").and_then(|v| v.as_bool()) {
1882                    // Boolean value
1883                    Ok(b.to_string())
1884                } else if let Some(v) = map.get("v").and_then(|v| v.as_str()) {
1885                    // null or undefined
1886                    Ok(v.to_string())
1887                } else {
1888                    // Unknown format, return JSON
1889                    Ok(result.value.to_string())
1890                }
1891            }
1892            _ => {
1893                // Fallback for unexpected formats
1894                Ok(result.value.to_string())
1895            }
1896        }
1897    }
1898
1899    /// Evaluates a JavaScript expression in the frame context with optional arguments.
1900    ///
1901    /// Executes the provided JavaScript expression within the frame's context and returns
1902    /// the result. The return value must be JSON-serializable.
1903    ///
1904    /// # Arguments
1905    ///
1906    /// * `expression` - JavaScript code to evaluate
1907    /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1908    ///
1909    /// # Returns
1910    ///
1911    /// The result as a `serde_json::Value`
1912    ///
1913    /// # Example
1914    ///
1915    /// ```ignore
1916    /// use serde_json::json;
1917    /// use playwright_rs::protocol::Playwright;
1918    ///
1919    /// #[tokio::main]
1920    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
1921    ///     let playwright = Playwright::launch().await?;
1922    ///     let browser = playwright.chromium().launch().await?;
1923    ///     let page = browser.new_page().await?;
1924    ///     let frame = page.main_frame().await?;
1925    ///
1926    ///     // Evaluate without arguments
1927    ///     let result = frame.evaluate::<()>("1 + 1", None).await?;
1928    ///
1929    ///     // Evaluate with argument
1930    ///     let arg = json!({"x": 5, "y": 3});
1931    ///     let result = frame.evaluate::<serde_json::Value>("(arg) => arg.x + arg.y", Some(&arg)).await?;
1932    ///     assert_eq!(result, json!(8));
1933    ///     Ok(())
1934    /// }
1935    /// ```
1936    ///
1937    /// See: <https://playwright.dev/docs/api/class-frame#frame-evaluate>
1938    pub async fn evaluate<T: serde::Serialize>(
1939        &self,
1940        expression: &str,
1941        arg: Option<&T>,
1942    ) -> Result<Value> {
1943        // Serialize the argument
1944        let serialized_arg = match arg {
1945            Some(a) => serialize_argument(a),
1946            None => serialize_null(),
1947        };
1948
1949        // Build the parameters
1950        let params = serde_json::json!({
1951            "expression": expression,
1952            "arg": serialized_arg
1953        });
1954
1955        // Send the evaluateExpression command
1956        #[derive(Deserialize)]
1957        struct EvaluateResult {
1958            value: serde_json::Value,
1959        }
1960
1961        let result: EvaluateResult = self.channel().send("evaluateExpression", params).await?;
1962
1963        // Deserialize the result using parse_result
1964        Ok(parse_result(&result.value))
1965    }
1966
1967    /// Adds a `<style>` tag into the page with the desired content.
1968    ///
1969    /// # Arguments
1970    ///
1971    /// * `options` - Style tag options (content, url, or path)
1972    ///
1973    /// At least one of `content`, `url`, or `path` must be specified.
1974    ///
1975    /// # Example
1976    ///
1977    /// ```no_run
1978    /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1979    /// # #[tokio::main]
1980    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1981    /// # let playwright = Playwright::launch().await?;
1982    /// # let browser = playwright.chromium().launch().await?;
1983    /// # let context = browser.new_context().await?;
1984    /// # let page = context.new_page().await?;
1985    /// # let frame = page.main_frame().await?;
1986    /// use playwright_rs::protocol::AddStyleTagOptions;
1987    ///
1988    /// // With inline CSS
1989    /// frame.add_style_tag(
1990    ///     AddStyleTagOptions::builder()
1991    ///         .content("body { background-color: red; }")
1992    ///         .build()
1993    /// ).await?;
1994    ///
1995    /// // With URL
1996    /// frame.add_style_tag(
1997    ///     AddStyleTagOptions::builder()
1998    ///         .url("https://example.com/style.css")
1999    ///         .build()
2000    /// ).await?;
2001    /// # Ok(())
2002    /// # }
2003    /// ```
2004    ///
2005    /// See: <https://playwright.dev/docs/api/class-frame#frame-add-style-tag>
2006    pub async fn add_style_tag(
2007        &self,
2008        options: crate::protocol::page::AddStyleTagOptions,
2009    ) -> Result<Arc<crate::protocol::ElementHandle>> {
2010        // Validate that at least one option is provided
2011        options.validate()?;
2012
2013        // Build protocol parameters
2014        let mut params = serde_json::json!({});
2015
2016        if let Some(content) = &options.content {
2017            params["content"] = serde_json::json!(content);
2018        }
2019
2020        if let Some(url) = &options.url {
2021            params["url"] = serde_json::json!(url);
2022        }
2023
2024        if let Some(path) = &options.path {
2025            // Read file content and send as content
2026            let css_content = tokio::fs::read_to_string(path).await.map_err(|e| {
2027                Error::InvalidArgument(format!("Failed to read CSS file '{}': {}", path, e))
2028            })?;
2029            params["content"] = serde_json::json!(css_content);
2030        }
2031
2032        #[derive(Deserialize)]
2033        struct AddStyleTagResponse {
2034            element: serde_json::Value,
2035        }
2036
2037        let response: AddStyleTagResponse = self.channel().send("addStyleTag", params).await?;
2038
2039        let guid = response.element["guid"].as_str().ok_or_else(|| {
2040            Error::ProtocolError("Element GUID missing in addStyleTag response".to_string())
2041        })?;
2042
2043        let connection = self.base.connection();
2044        let handle: crate::protocol::ElementHandle = connection
2045            .get_typed::<crate::protocol::ElementHandle>(guid)
2046            .await?;
2047
2048        Ok(Arc::new(handle))
2049    }
2050
2051    /// Dispatches a DOM event on the element matching the selector.
2052    ///
2053    /// Unlike clicking or typing, `dispatch_event` directly sends the event without
2054    /// performing any actionability checks. It still waits for the element to be present
2055    /// in the DOM.
2056    ///
2057    /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
2058    pub(crate) async fn locator_dispatch_event(
2059        &self,
2060        selector: &str,
2061        type_: &str,
2062        event_init: Option<serde_json::Value>,
2063    ) -> Result<()> {
2064        // Serialize eventInit using Playwright's protocol argument format.
2065        // If None, use {"value": {"v": "undefined"}, "handles": []}.
2066        let event_init_serialized = match event_init {
2067            Some(v) => serialize_argument(&v),
2068            None => serde_json::json!({"value": {"v": "undefined"}, "handles": []}),
2069        };
2070
2071        let params = serde_json::json!({
2072            "selector": selector,
2073            "type": type_,
2074            "eventInit": event_init_serialized,
2075            "strict": true,
2076            "timeout": crate::DEFAULT_TIMEOUT_MS
2077        });
2078
2079        self.channel().send_no_result("dispatchEvent", params).await
2080    }
2081
2082    /// Returns the bounding box of the element matching the selector, or None if not visible.
2083    ///
2084    /// The bounding box is returned in pixels. If the element is not visible (e.g.,
2085    /// `display: none`), returns `None`.
2086    ///
2087    /// Implemented via ElementHandle because `boundingBox` is an ElementHandle-level
2088    /// protocol method, not a Frame-level method.
2089    ///
2090    /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
2091    pub(crate) async fn locator_bounding_box(
2092        &self,
2093        selector: &str,
2094    ) -> Result<Option<crate::protocol::locator::BoundingBox>> {
2095        let element = self.query_selector(selector).await?;
2096        match element {
2097            Some(handle) => handle.bounding_box().await,
2098            None => Ok(None),
2099        }
2100    }
2101
2102    /// Scrolls the element into view if it is not already visible in the viewport.
2103    ///
2104    /// Implemented via ElementHandle because `scrollIntoViewIfNeeded` is an
2105    /// ElementHandle-level protocol method, not a Frame-level method.
2106    ///
2107    /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
2108    pub(crate) async fn locator_scroll_into_view_if_needed(&self, selector: &str) -> Result<()> {
2109        let element = self.query_selector(selector).await?;
2110        match element {
2111            Some(handle) => handle.scroll_into_view_if_needed().await,
2112            None => Err(crate::error::Error::ElementNotFound(format!(
2113                "Element not found: {}",
2114                selector
2115            ))),
2116        }
2117    }
2118
2119    /// Adds a `<script>` tag into the frame with the desired content.
2120    ///
2121    /// # Arguments
2122    ///
2123    /// * `options` - Script tag options (content, url, or path)
2124    ///
2125    /// At least one of `content`, `url`, or `path` must be specified.
2126    ///
2127    /// See: <https://playwright.dev/docs/api/class-frame#frame-add-script-tag>
2128    pub async fn add_script_tag(
2129        &self,
2130        options: crate::protocol::page::AddScriptTagOptions,
2131    ) -> Result<Arc<crate::protocol::ElementHandle>> {
2132        // Validate that at least one option is provided
2133        options.validate()?;
2134
2135        // Build protocol parameters
2136        let mut params = serde_json::json!({});
2137
2138        if let Some(content) = &options.content {
2139            params["content"] = serde_json::json!(content);
2140        }
2141
2142        if let Some(url) = &options.url {
2143            params["url"] = serde_json::json!(url);
2144        }
2145
2146        if let Some(path) = &options.path {
2147            // Read file content and send as content
2148            let js_content = tokio::fs::read_to_string(path).await.map_err(|e| {
2149                Error::InvalidArgument(format!("Failed to read JS file '{}': {}", path, e))
2150            })?;
2151            params["content"] = serde_json::json!(js_content);
2152        }
2153
2154        if let Some(type_) = &options.type_ {
2155            params["type"] = serde_json::json!(type_);
2156        }
2157
2158        #[derive(Deserialize)]
2159        struct AddScriptTagResponse {
2160            element: serde_json::Value,
2161        }
2162
2163        let response: AddScriptTagResponse = self.channel().send("addScriptTag", params).await?;
2164
2165        let guid = response.element["guid"].as_str().ok_or_else(|| {
2166            Error::ProtocolError("Element GUID missing in addScriptTag response".to_string())
2167        })?;
2168
2169        let connection = self.base.connection();
2170        let handle: crate::protocol::ElementHandle = connection
2171            .get_typed::<crate::protocol::ElementHandle>(guid)
2172            .await?;
2173
2174        Ok(Arc::new(handle))
2175    }
2176}
2177
2178impl ChannelOwner for Frame {
2179    fn guid(&self) -> &str {
2180        self.base.guid()
2181    }
2182
2183    fn type_name(&self) -> &str {
2184        self.base.type_name()
2185    }
2186
2187    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2188        self.base.parent()
2189    }
2190
2191    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2192        self.base.connection()
2193    }
2194
2195    fn initializer(&self) -> &Value {
2196        self.base.initializer()
2197    }
2198
2199    fn channel(&self) -> &Channel {
2200        self.base.channel()
2201    }
2202
2203    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2204        self.base.dispose(reason)
2205    }
2206
2207    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2208        self.base.adopt(child)
2209    }
2210
2211    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2212        self.base.add_child(guid, child)
2213    }
2214
2215    fn remove_child(&self, guid: &str) {
2216        self.base.remove_child(guid)
2217    }
2218
2219    fn on_event(&self, method: &str, params: Value) {
2220        match method {
2221            "navigated" => {
2222                // Update frame's URL when navigation occurs (including hash changes)
2223                if let Some(url_value) = params.get("url")
2224                    && let Some(url_str) = url_value.as_str()
2225                {
2226                    // Update frame's URL
2227                    if let Ok(mut url) = self.url.write() {
2228                        *url = url_str.to_string();
2229                    }
2230                }
2231            }
2232            "detached" => {
2233                // Mark this frame as detached
2234                if let Ok(mut flag) = self.is_detached.write() {
2235                    *flag = true;
2236                }
2237            }
2238            _ => {
2239                // Other events will be handled in future phases
2240                // Events: loadstate, etc.
2241            }
2242        }
2243    }
2244
2245    fn was_collected(&self) -> bool {
2246        self.base.was_collected()
2247    }
2248
2249    fn as_any(&self) -> &dyn Any {
2250        self
2251    }
2252}
2253
2254impl std::fmt::Debug for Frame {
2255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2256        f.debug_struct("Frame").field("guid", &self.guid()).finish()
2257    }
2258}
2259
2260/// Simple glob pattern matching for URL patterns.
2261///
2262/// Supports `*` (matches any characters except `/`) and `**` (matches any characters including `/`).
2263/// This matches Playwright's URL glob pattern behavior.
2264fn glob_match(pattern: &str, text: &str) -> bool {
2265    let regex_str = pattern
2266        .replace('.', "\\.")
2267        .replace("**", "\x00") // placeholder for **
2268        .replace('*', "[^/]*")
2269        .replace('\x00', ".*"); // restore ** as .*
2270    let regex_str = format!("^{}$", regex_str);
2271    regex::Regex::new(&regex_str)
2272        .map(|re| re.is_match(text))
2273        .unwrap_or(false)
2274}