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