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