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