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