Skip to main content

playwright_rs/protocol/
frame.rs

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