Skip to main content

playwright_rs/protocol/
frame.rs

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