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 serde::Deserialize;
12use serde_json::Value;
13use std::any::Any;
14use std::sync::{Arc, RwLock};
15
16/// Frame represents a frame within a page.
17///
18/// Every page has a main frame, and pages can have additional child frames (iframes).
19/// Frame is where navigation, selector queries, and DOM operations actually happen.
20///
21/// In Playwright's architecture, Page delegates navigation and interaction methods to Frame.
22///
23/// See: <https://playwright.dev/docs/api/class-frame>
24#[derive(Clone)]
25pub struct Frame {
26    base: ChannelOwnerImpl,
27    /// Current URL of the frame
28    /// Wrapped in RwLock to allow updates from events
29    url: Arc<RwLock<String>>,
30}
31
32impl Frame {
33    /// Creates a new Frame from protocol initialization
34    ///
35    /// This is called by the object factory when the server sends a `__create__` message
36    /// for a Frame object.
37    pub fn new(
38        parent: Arc<dyn ChannelOwner>,
39        type_name: String,
40        guid: Arc<str>,
41        initializer: Value,
42    ) -> Result<Self> {
43        let base = ChannelOwnerImpl::new(
44            ParentOrConnection::Parent(parent),
45            type_name,
46            guid,
47            initializer.clone(),
48        );
49
50        // Extract initial URL from initializer if available
51        let initial_url = initializer
52            .get("url")
53            .and_then(|v| v.as_str())
54            .unwrap_or("about:blank")
55            .to_string();
56
57        let url = Arc::new(RwLock::new(initial_url));
58
59        Ok(Self { base, url })
60    }
61
62    /// Returns the channel for sending protocol messages
63    fn channel(&self) -> &Channel {
64        self.base.channel()
65    }
66
67    /// Returns the current URL of the frame.
68    ///
69    /// This returns the last committed URL. Initially, frames are at "about:blank".
70    ///
71    /// See: <https://playwright.dev/docs/api/class-frame#frame-url>
72    pub fn url(&self) -> String {
73        self.url.read().unwrap().clone()
74    }
75
76    /// Navigates the frame to the specified URL.
77    ///
78    /// This is the actual protocol method for navigation. Page.goto() delegates to this.
79    ///
80    /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
81    /// about:blank). This matches Playwright's behavior across all language bindings.
82    ///
83    /// # Arguments
84    ///
85    /// * `url` - The URL to navigate to
86    /// * `options` - Optional navigation options (timeout, wait_until)
87    ///
88    /// See: <https://playwright.dev/docs/api/class-frame#frame-goto>
89    pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
90        // Build params manually using json! macro
91        let mut params = serde_json::json!({
92            "url": url,
93        });
94
95        // Add optional parameters
96        if let Some(opts) = options {
97            if let Some(timeout) = opts.timeout {
98                params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
99            } else {
100                // Default timeout required in Playwright 1.56.1+
101                params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
102            }
103            if let Some(wait_until) = opts.wait_until {
104                params["waitUntil"] = serde_json::json!(wait_until.as_str());
105            }
106        } else {
107            // No options provided, set default timeout (required in Playwright 1.56.1+)
108            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
109        }
110
111        // Send goto RPC to Frame
112        // The server returns { "response": { "guid": "..." } } or null
113        #[derive(Deserialize)]
114        struct GotoResponse {
115            response: Option<ResponseReference>,
116        }
117
118        #[derive(Deserialize)]
119        struct ResponseReference {
120            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
121            guid: Arc<str>,
122        }
123
124        let goto_result: GotoResponse = self.channel().send("goto", params).await?;
125
126        // If navigation returned a response, get the Response object from the connection
127        if let Some(response_ref) = goto_result.response {
128            // The server returns a Response GUID, but the __create__ message might not have
129            // arrived yet. Retry a few times to wait for the object to be created.
130            // TODO(Phase 4+): Implement proper GUID replacement like Python's _replace_guids_with_channels
131            //   - Eliminates retry loop for better performance
132            //   - See: playwright-python's _replace_guids_with_channels method
133            let response_arc = {
134                let mut attempts = 0;
135                let max_attempts = 20; // 20 * 50ms = 1 second max wait
136                loop {
137                    match self.connection().get_object(&response_ref.guid).await {
138                        Ok(obj) => break obj,
139                        Err(_) if attempts < max_attempts => {
140                            attempts += 1;
141                            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
142                        }
143                        Err(e) => return Err(e),
144                    }
145                }
146            };
147
148            // Extract Response data from the initializer, and store the Arc for RPC calls
149            // (body(), rawHeaders(), headerValue()) that need to contact the server.
150            let initializer = response_arc.initializer();
151
152            // Extract response data from initializer
153            let status = initializer["status"].as_u64().ok_or_else(|| {
154                crate::error::Error::ProtocolError("Response missing status".to_string())
155            })? as u16;
156
157            // Convert headers from array format to HashMap
158            let headers = initializer["headers"]
159                .as_array()
160                .ok_or_else(|| {
161                    crate::error::Error::ProtocolError("Response missing headers".to_string())
162                })?
163                .iter()
164                .filter_map(|h| {
165                    let name = h["name"].as_str()?;
166                    let value = h["value"].as_str()?;
167                    Some((name.to_string(), value.to_string()))
168                })
169                .collect();
170
171            Ok(Some(Response::new(
172                initializer["url"]
173                    .as_str()
174                    .ok_or_else(|| {
175                        crate::error::Error::ProtocolError("Response missing url".to_string())
176                    })?
177                    .to_string(),
178                status,
179                initializer["statusText"].as_str().unwrap_or("").to_string(),
180                headers,
181                Some(response_arc),
182            )))
183        } else {
184            // Navigation returned null (e.g., data URLs, about:blank)
185            // This is a valid result, not an error
186            Ok(None)
187        }
188    }
189
190    /// Returns the frame's title.
191    ///
192    /// See: <https://playwright.dev/docs/api/class-frame#frame-title>
193    pub async fn title(&self) -> Result<String> {
194        #[derive(Deserialize)]
195        struct TitleResponse {
196            value: String,
197        }
198
199        let response: TitleResponse = self.channel().send("title", serde_json::json!({})).await?;
200        Ok(response.value)
201    }
202
203    /// Returns the full HTML content of the frame, including the DOCTYPE.
204    ///
205    /// See: <https://playwright.dev/docs/api/class-frame#frame-content>
206    pub async fn content(&self) -> Result<String> {
207        #[derive(Deserialize)]
208        struct ContentResponse {
209            value: String,
210        }
211
212        let response: ContentResponse = self
213            .channel()
214            .send("content", serde_json::json!({}))
215            .await?;
216        Ok(response.value)
217    }
218
219    /// Sets the content of the frame.
220    ///
221    /// See: <https://playwright.dev/docs/api/class-frame#frame-set-content>
222    pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
223        let mut params = serde_json::json!({
224            "html": html,
225        });
226
227        if let Some(opts) = options {
228            if let Some(timeout) = opts.timeout {
229                params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
230            } else {
231                params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
232            }
233            if let Some(wait_until) = opts.wait_until {
234                params["waitUntil"] = serde_json::json!(wait_until.as_str());
235            }
236        } else {
237            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
238        }
239
240        self.channel().send_no_result("setContent", params).await
241    }
242
243    /// Waits for the required load state to be reached.
244    ///
245    /// Playwright's protocol doesn't expose `waitForLoadState` as a server-side command —
246    /// it's implemented client-side using lifecycle events. We implement it by polling
247    /// `document.readyState` via JavaScript evaluation.
248    ///
249    /// See: <https://playwright.dev/docs/api/class-frame#frame-wait-for-load-state>
250    pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
251        let target_state = state.unwrap_or(WaitUntil::Load);
252
253        let js_check = match target_state {
254            // "load" means the full page has loaded (readyState === "complete")
255            WaitUntil::Load => "document.readyState === 'complete'",
256            // "domcontentloaded" means DOM is ready (readyState !== "loading")
257            WaitUntil::DomContentLoaded => "document.readyState !== 'loading'",
258            // "networkidle" has no direct readyState equivalent; we approximate
259            // by checking "complete" (same as Load)
260            WaitUntil::NetworkIdle => "document.readyState === 'complete'",
261            // "commit" means any response has been received (readyState !== "loading" at minimum)
262            WaitUntil::Commit => "document.readyState !== 'loading'",
263        };
264
265        let timeout_ms = crate::DEFAULT_TIMEOUT_MS as u64;
266        let poll_interval = std::time::Duration::from_millis(50);
267        let start = std::time::Instant::now();
268
269        loop {
270            #[derive(Deserialize)]
271            struct EvalResponse {
272                value: serde_json::Value,
273            }
274
275            let result: EvalResponse = self
276                .channel()
277                .send(
278                    "evaluateExpression",
279                    serde_json::json!({
280                        "expression": js_check,
281                        "isFunction": false,
282                        "arg": crate::protocol::serialize_null(),
283                    }),
284                )
285                .await?;
286
287            // Playwright protocol returns booleans as {"b": true/false}
288            let is_ready = result
289                .value
290                .as_object()
291                .and_then(|m| m.get("b"))
292                .and_then(|v| v.as_bool())
293                .unwrap_or(false);
294
295            if is_ready {
296                return Ok(());
297            }
298
299            if start.elapsed().as_millis() as u64 >= timeout_ms {
300                return Err(crate::error::Error::Timeout(format!(
301                    "wait_for_load_state({}) timed out after {}ms",
302                    target_state.as_str(),
303                    timeout_ms
304                )));
305            }
306
307            tokio::time::sleep(poll_interval).await;
308        }
309    }
310
311    /// Waits for the frame to navigate to a URL matching the given string or glob pattern.
312    ///
313    /// Playwright's protocol doesn't expose `waitForURL` as a server-side command —
314    /// it's implemented client-side. We implement it by polling `window.location.href`.
315    ///
316    /// See: <https://playwright.dev/docs/api/class-frame#frame-wait-for-url>
317    pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
318        let timeout_ms = options
319            .as_ref()
320            .and_then(|o| o.timeout)
321            .map(|d| d.as_millis() as u64)
322            .unwrap_or(crate::DEFAULT_TIMEOUT_MS as u64);
323
324        // Convert glob pattern to regex for matching
325        // Playwright supports string (exact), glob (**), and regex patterns
326        // We support exact string and basic glob patterns
327        let is_glob = url.contains('*');
328
329        let poll_interval = std::time::Duration::from_millis(50);
330        let start = std::time::Instant::now();
331
332        loop {
333            let current_url = self.url();
334
335            let matches = if is_glob {
336                glob_match(url, &current_url)
337            } else {
338                current_url == url
339            };
340
341            if matches {
342                // URL matches — optionally wait for load state
343                if let Some(ref opts) = options {
344                    if let Some(wait_until) = opts.wait_until {
345                        self.wait_for_load_state(Some(wait_until)).await?;
346                    }
347                }
348                return Ok(());
349            }
350
351            if start.elapsed().as_millis() as u64 >= timeout_ms {
352                return Err(crate::error::Error::Timeout(format!(
353                    "wait_for_url({}) timed out after {}ms, current URL: {}",
354                    url, timeout_ms, current_url
355                )));
356            }
357
358            tokio::time::sleep(poll_interval).await;
359        }
360    }
361
362    /// Returns the first element matching the selector, or None if not found.
363    ///
364    /// See: <https://playwright.dev/docs/api/class-frame#frame-query-selector>
365    pub async fn query_selector(
366        &self,
367        selector: &str,
368    ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
369        let response: serde_json::Value = self
370            .channel()
371            .send(
372                "querySelector",
373                serde_json::json!({
374                    "selector": selector
375                }),
376            )
377            .await?;
378
379        // Check if response is empty (no element found)
380        if response.as_object().map(|o| o.is_empty()).unwrap_or(true) {
381            return Ok(None);
382        }
383
384        // Try different possible field names
385        let element_value = if let Some(elem) = response.get("element") {
386            elem
387        } else if let Some(elem) = response.get("handle") {
388            elem
389        } else {
390            // Maybe the response IS the guid object itself
391            &response
392        };
393
394        if element_value.is_null() {
395            return Ok(None);
396        }
397
398        // Element response contains { guid: "elementHandle@123" }
399        let guid = element_value["guid"].as_str().ok_or_else(|| {
400            crate::error::Error::ProtocolError("Element GUID missing".to_string())
401        })?;
402
403        // Look up the ElementHandle object in the connection's object registry
404        let connection = self.base.connection();
405        let element = connection.get_object(guid).await?;
406
407        // Downcast to ElementHandle
408        let handle = element
409            .as_any()
410            .downcast_ref::<crate::protocol::ElementHandle>()
411            .map(|e| Arc::new(e.clone()))
412            .ok_or_else(|| {
413                crate::error::Error::ProtocolError(format!(
414                    "Object {} is not an ElementHandle",
415                    guid
416                ))
417            })?;
418
419        Ok(Some(handle))
420    }
421
422    /// Returns all elements matching the selector.
423    ///
424    /// See: <https://playwright.dev/docs/api/class-frame#frame-query-selector-all>
425    pub async fn query_selector_all(
426        &self,
427        selector: &str,
428    ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
429        #[derive(Deserialize)]
430        struct QueryAllResponse {
431            elements: Vec<serde_json::Value>,
432        }
433
434        let response: QueryAllResponse = self
435            .channel()
436            .send(
437                "querySelectorAll",
438                serde_json::json!({
439                    "selector": selector
440                }),
441            )
442            .await?;
443
444        // Convert GUID responses to ElementHandle objects
445        let connection = self.base.connection();
446        let mut handles = Vec::new();
447
448        for element_value in response.elements {
449            let guid = element_value["guid"].as_str().ok_or_else(|| {
450                crate::error::Error::ProtocolError("Element GUID missing".to_string())
451            })?;
452
453            let element = connection.get_object(guid).await?;
454
455            let handle = element
456                .as_any()
457                .downcast_ref::<crate::protocol::ElementHandle>()
458                .map(|e| Arc::new(e.clone()))
459                .ok_or_else(|| {
460                    crate::error::Error::ProtocolError(format!(
461                        "Object {} is not an ElementHandle",
462                        guid
463                    ))
464                })?;
465
466            handles.push(handle);
467        }
468
469        Ok(handles)
470    }
471
472    // Locator delegate methods
473    // These are called by Locator to perform actual queries
474
475    /// Returns the number of elements matching the selector.
476    pub(crate) async fn locator_count(&self, selector: &str) -> Result<usize> {
477        // Use querySelectorAll which returns array of element handles
478        #[derive(Deserialize)]
479        struct QueryAllResponse {
480            elements: Vec<serde_json::Value>,
481        }
482
483        let response: QueryAllResponse = self
484            .channel()
485            .send(
486                "querySelectorAll",
487                serde_json::json!({
488                    "selector": selector
489                }),
490            )
491            .await?;
492
493        Ok(response.elements.len())
494    }
495
496    /// Returns the text content of the element.
497    pub(crate) async fn locator_text_content(&self, selector: &str) -> Result<Option<String>> {
498        #[derive(Deserialize)]
499        struct TextContentResponse {
500            value: Option<String>,
501        }
502
503        let response: TextContentResponse = self
504            .channel()
505            .send(
506                "textContent",
507                serde_json::json!({
508                    "selector": selector,
509                    "strict": true,
510                    "timeout": crate::DEFAULT_TIMEOUT_MS
511                }),
512            )
513            .await?;
514
515        Ok(response.value)
516    }
517
518    /// Returns the inner text of the element.
519    pub(crate) async fn locator_inner_text(&self, selector: &str) -> Result<String> {
520        #[derive(Deserialize)]
521        struct InnerTextResponse {
522            value: String,
523        }
524
525        let response: InnerTextResponse = self
526            .channel()
527            .send(
528                "innerText",
529                serde_json::json!({
530                    "selector": selector,
531                    "strict": true,
532                    "timeout": crate::DEFAULT_TIMEOUT_MS
533                }),
534            )
535            .await?;
536
537        Ok(response.value)
538    }
539
540    /// Returns the inner HTML of the element.
541    pub(crate) async fn locator_inner_html(&self, selector: &str) -> Result<String> {
542        #[derive(Deserialize)]
543        struct InnerHTMLResponse {
544            value: String,
545        }
546
547        let response: InnerHTMLResponse = self
548            .channel()
549            .send(
550                "innerHTML",
551                serde_json::json!({
552                    "selector": selector,
553                    "strict": true,
554                    "timeout": crate::DEFAULT_TIMEOUT_MS
555                }),
556            )
557            .await?;
558
559        Ok(response.value)
560    }
561
562    /// Returns the value of the specified attribute.
563    pub(crate) async fn locator_get_attribute(
564        &self,
565        selector: &str,
566        name: &str,
567    ) -> Result<Option<String>> {
568        #[derive(Deserialize)]
569        struct GetAttributeResponse {
570            value: Option<String>,
571        }
572
573        let response: GetAttributeResponse = self
574            .channel()
575            .send(
576                "getAttribute",
577                serde_json::json!({
578                    "selector": selector,
579                    "name": name,
580                    "strict": true,
581                    "timeout": crate::DEFAULT_TIMEOUT_MS
582                }),
583            )
584            .await?;
585
586        Ok(response.value)
587    }
588
589    /// Returns whether the element is visible.
590    pub(crate) async fn locator_is_visible(&self, selector: &str) -> Result<bool> {
591        #[derive(Deserialize)]
592        struct IsVisibleResponse {
593            value: bool,
594        }
595
596        let response: IsVisibleResponse = self
597            .channel()
598            .send(
599                "isVisible",
600                serde_json::json!({
601                    "selector": selector,
602                    "strict": true,
603                    "timeout": crate::DEFAULT_TIMEOUT_MS
604                }),
605            )
606            .await?;
607
608        Ok(response.value)
609    }
610
611    /// Returns whether the element is enabled.
612    pub(crate) async fn locator_is_enabled(&self, selector: &str) -> Result<bool> {
613        #[derive(Deserialize)]
614        struct IsEnabledResponse {
615            value: bool,
616        }
617
618        let response: IsEnabledResponse = self
619            .channel()
620            .send(
621                "isEnabled",
622                serde_json::json!({
623                    "selector": selector,
624                    "strict": true,
625                    "timeout": crate::DEFAULT_TIMEOUT_MS
626                }),
627            )
628            .await?;
629
630        Ok(response.value)
631    }
632
633    /// Returns whether the checkbox or radio button is checked.
634    pub(crate) async fn locator_is_checked(&self, selector: &str) -> Result<bool> {
635        #[derive(Deserialize)]
636        struct IsCheckedResponse {
637            value: bool,
638        }
639
640        let response: IsCheckedResponse = self
641            .channel()
642            .send(
643                "isChecked",
644                serde_json::json!({
645                    "selector": selector,
646                    "strict": true,
647                    "timeout": crate::DEFAULT_TIMEOUT_MS
648                }),
649            )
650            .await?;
651
652        Ok(response.value)
653    }
654
655    /// Returns whether the element is editable.
656    pub(crate) async fn locator_is_editable(&self, selector: &str) -> Result<bool> {
657        #[derive(Deserialize)]
658        struct IsEditableResponse {
659            value: bool,
660        }
661
662        let response: IsEditableResponse = self
663            .channel()
664            .send(
665                "isEditable",
666                serde_json::json!({
667                    "selector": selector,
668                    "strict": true,
669                    "timeout": crate::DEFAULT_TIMEOUT_MS
670                }),
671            )
672            .await?;
673
674        Ok(response.value)
675    }
676
677    /// Returns whether the element is hidden.
678    pub(crate) async fn locator_is_hidden(&self, selector: &str) -> Result<bool> {
679        #[derive(Deserialize)]
680        struct IsHiddenResponse {
681            value: bool,
682        }
683
684        let response: IsHiddenResponse = self
685            .channel()
686            .send(
687                "isHidden",
688                serde_json::json!({
689                    "selector": selector,
690                    "strict": true,
691                    "timeout": crate::DEFAULT_TIMEOUT_MS
692                }),
693            )
694            .await?;
695
696        Ok(response.value)
697    }
698
699    /// Returns whether the element is disabled.
700    pub(crate) async fn locator_is_disabled(&self, selector: &str) -> Result<bool> {
701        #[derive(Deserialize)]
702        struct IsDisabledResponse {
703            value: bool,
704        }
705
706        let response: IsDisabledResponse = self
707            .channel()
708            .send(
709                "isDisabled",
710                serde_json::json!({
711                    "selector": selector,
712                    "strict": true,
713                    "timeout": crate::DEFAULT_TIMEOUT_MS
714                }),
715            )
716            .await?;
717
718        Ok(response.value)
719    }
720
721    /// Returns whether the element is focused (currently has focus).
722    ///
723    /// This implementation checks if the element is the activeElement in the DOM
724    /// using JavaScript evaluation, since Playwright doesn't expose isFocused() at
725    /// the protocol level.
726    pub(crate) async fn locator_is_focused(&self, selector: &str) -> Result<bool> {
727        #[derive(Deserialize)]
728        struct EvaluateResult {
729            value: serde_json::Value,
730        }
731
732        // Use JavaScript to check if the element is the active element
733        // The script queries the DOM and returns true/false
734        let script = r#"selector => {
735                const elements = document.querySelectorAll(selector);
736                if (elements.length === 0) return false;
737                const element = elements[0];
738                return document.activeElement === element;
739            }"#;
740
741        let params = serde_json::json!({
742            "expression": script,
743            "arg": {
744                "value": {"s": selector},
745                "handles": []
746            }
747        });
748
749        let result: EvaluateResult = self.channel().send("evaluateExpression", params).await?;
750
751        // Playwright protocol returns booleans as {"b": true} or {"b": false}
752        if let serde_json::Value::Object(map) = &result.value {
753            if let Some(b) = map.get("b").and_then(|v| v.as_bool()) {
754                return Ok(b);
755            }
756        }
757
758        // Fallback: check if the string representation is "true"
759        Ok(result.value.to_string().to_lowercase().contains("true"))
760    }
761
762    // Action delegate methods
763
764    /// Clicks the element matching the selector.
765    pub(crate) async fn locator_click(
766        &self,
767        selector: &str,
768        options: Option<crate::protocol::ClickOptions>,
769    ) -> Result<()> {
770        let mut params = serde_json::json!({
771            "selector": selector,
772            "strict": true
773        });
774
775        if let Some(opts) = options {
776            let opts_json = opts.to_json();
777            if let Some(obj) = params.as_object_mut() {
778                if let Some(opts_obj) = opts_json.as_object() {
779                    obj.extend(opts_obj.clone());
780                }
781            }
782        } else {
783            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
784        }
785
786        self.channel()
787            .send_no_result("click", params)
788            .await
789            .map_err(|e| match e {
790                Error::Timeout(msg) => {
791                    Error::Timeout(format!("{} (selector: '{}')", msg, selector))
792                }
793                other => other,
794            })
795    }
796
797    /// Double clicks the element matching the selector.
798    pub(crate) async fn locator_dblclick(
799        &self,
800        selector: &str,
801        options: Option<crate::protocol::ClickOptions>,
802    ) -> Result<()> {
803        let mut params = serde_json::json!({
804            "selector": selector,
805            "strict": true
806        });
807
808        if let Some(opts) = options {
809            let opts_json = opts.to_json();
810            if let Some(obj) = params.as_object_mut() {
811                if let Some(opts_obj) = opts_json.as_object() {
812                    obj.extend(opts_obj.clone());
813                }
814            }
815        } else {
816            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
817        }
818
819        self.channel().send_no_result("dblclick", params).await
820    }
821
822    /// Fills the element with text.
823    pub(crate) async fn locator_fill(
824        &self,
825        selector: &str,
826        text: &str,
827        options: Option<crate::protocol::FillOptions>,
828    ) -> Result<()> {
829        let mut params = serde_json::json!({
830            "selector": selector,
831            "value": text,
832            "strict": true
833        });
834
835        if let Some(opts) = options {
836            let opts_json = opts.to_json();
837            if let Some(obj) = params.as_object_mut() {
838                if let Some(opts_obj) = opts_json.as_object() {
839                    obj.extend(opts_obj.clone());
840                }
841            }
842        } else {
843            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
844        }
845
846        self.channel().send_no_result("fill", params).await
847    }
848
849    /// Clears the element's value.
850    pub(crate) async fn locator_clear(
851        &self,
852        selector: &str,
853        options: Option<crate::protocol::FillOptions>,
854    ) -> Result<()> {
855        let mut params = serde_json::json!({
856            "selector": selector,
857            "value": "",
858            "strict": true
859        });
860
861        if let Some(opts) = options {
862            let opts_json = opts.to_json();
863            if let Some(obj) = params.as_object_mut() {
864                if let Some(opts_obj) = opts_json.as_object() {
865                    obj.extend(opts_obj.clone());
866                }
867            }
868        } else {
869            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
870        }
871
872        self.channel().send_no_result("fill", params).await
873    }
874
875    /// Presses a key on the element.
876    pub(crate) async fn locator_press(
877        &self,
878        selector: &str,
879        key: &str,
880        options: Option<crate::protocol::PressOptions>,
881    ) -> Result<()> {
882        let mut params = serde_json::json!({
883            "selector": selector,
884            "key": key,
885            "strict": true
886        });
887
888        if let Some(opts) = options {
889            let opts_json = opts.to_json();
890            if let Some(obj) = params.as_object_mut() {
891                if let Some(opts_obj) = opts_json.as_object() {
892                    obj.extend(opts_obj.clone());
893                }
894            }
895        } else {
896            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
897        }
898
899        self.channel().send_no_result("press", params).await
900    }
901
902    /// Sets focus on the element matching the selector.
903    pub(crate) async fn locator_focus(&self, selector: &str) -> Result<()> {
904        self.channel()
905            .send_no_result(
906                "focus",
907                serde_json::json!({
908                    "selector": selector,
909                    "strict": true,
910                    "timeout": crate::DEFAULT_TIMEOUT_MS
911                }),
912            )
913            .await
914    }
915
916    /// Removes focus from the element matching the selector.
917    pub(crate) async fn locator_blur(&self, selector: &str) -> Result<()> {
918        self.channel()
919            .send_no_result(
920                "blur",
921                serde_json::json!({
922                    "selector": selector,
923                    "strict": true,
924                    "timeout": crate::DEFAULT_TIMEOUT_MS
925                }),
926            )
927            .await
928    }
929
930    /// Types text into the element character by character.
931    ///
932    /// Uses the Playwright protocol `"type"` message (the legacy name for pressSequentially).
933    pub(crate) async fn locator_press_sequentially(
934        &self,
935        selector: &str,
936        text: &str,
937        options: Option<crate::protocol::PressSequentiallyOptions>,
938    ) -> Result<()> {
939        let mut params = serde_json::json!({
940            "selector": selector,
941            "text": text,
942            "strict": true
943        });
944
945        if let Some(opts) = options {
946            let opts_json = opts.to_json();
947            if let Some(obj) = params.as_object_mut() {
948                if let Some(opts_obj) = opts_json.as_object() {
949                    obj.extend(opts_obj.clone());
950                }
951            }
952        } else {
953            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
954        }
955
956        self.channel().send_no_result("type", params).await
957    }
958
959    /// Returns the inner text of all elements matching the selector.
960    pub(crate) async fn locator_all_inner_texts(&self, selector: &str) -> Result<Vec<String>> {
961        #[derive(serde::Deserialize)]
962        struct EvaluateResult {
963            value: serde_json::Value,
964        }
965
966        // The Playwright protocol's evalOnSelectorAll requires an `arg` field.
967        // We pass a null argument since our expression doesn't use one.
968        let params = serde_json::json!({
969            "selector": selector,
970            "expression": "ee => ee.map(e => e.innerText)",
971            "isFunction": true,
972            "arg": {
973                "value": {"v": "null"},
974                "handles": []
975            }
976        });
977
978        let result: EvaluateResult = self.channel().send("evalOnSelectorAll", params).await?;
979
980        Self::parse_string_array(result.value)
981    }
982
983    /// Returns the text content of all elements matching the selector.
984    pub(crate) async fn locator_all_text_contents(&self, selector: &str) -> Result<Vec<String>> {
985        #[derive(serde::Deserialize)]
986        struct EvaluateResult {
987            value: serde_json::Value,
988        }
989
990        // The Playwright protocol's evalOnSelectorAll requires an `arg` field.
991        // We pass a null argument since our expression doesn't use one.
992        let params = serde_json::json!({
993            "selector": selector,
994            "expression": "ee => ee.map(e => e.textContent || '')",
995            "isFunction": true,
996            "arg": {
997                "value": {"v": "null"},
998                "handles": []
999            }
1000        });
1001
1002        let result: EvaluateResult = self.channel().send("evalOnSelectorAll", params).await?;
1003
1004        Self::parse_string_array(result.value)
1005    }
1006
1007    /// Performs a touch-tap on the element matching the selector.
1008    ///
1009    /// Sends touch events rather than mouse events. Requires the browser context to be
1010    /// created with `has_touch: true`.
1011    ///
1012    /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1013    pub(crate) async fn locator_tap(
1014        &self,
1015        selector: &str,
1016        options: Option<crate::protocol::TapOptions>,
1017    ) -> Result<()> {
1018        let mut params = serde_json::json!({
1019            "selector": selector,
1020            "strict": true
1021        });
1022
1023        if let Some(opts) = options {
1024            let opts_json = opts.to_json();
1025            if let Some(obj) = params.as_object_mut() {
1026                if let Some(opts_obj) = opts_json.as_object() {
1027                    obj.extend(opts_obj.clone());
1028                }
1029            }
1030        } else {
1031            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1032        }
1033
1034        self.channel().send_no_result("tap", params).await
1035    }
1036
1037    /// Drags the source element onto the target element.
1038    ///
1039    /// Both selectors must resolve to elements in this frame.
1040    ///
1041    /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1042    pub(crate) async fn locator_drag_to(
1043        &self,
1044        source_selector: &str,
1045        target_selector: &str,
1046        options: Option<crate::protocol::DragToOptions>,
1047    ) -> Result<()> {
1048        let mut params = serde_json::json!({
1049            "source": source_selector,
1050            "target": target_selector,
1051            "strict": true
1052        });
1053
1054        if let Some(opts) = options {
1055            let opts_json = opts.to_json();
1056            if let Some(obj) = params.as_object_mut() {
1057                if let Some(opts_obj) = opts_json.as_object() {
1058                    obj.extend(opts_obj.clone());
1059                }
1060            }
1061        } else {
1062            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1063        }
1064
1065        self.channel().send_no_result("dragAndDrop", params).await
1066    }
1067
1068    /// Waits for the element to satisfy a state condition.
1069    ///
1070    /// Uses Playwright's `waitForSelector` RPC. The element state defaults to `visible`
1071    /// if not specified.
1072    ///
1073    /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1074    pub(crate) async fn locator_wait_for(
1075        &self,
1076        selector: &str,
1077        options: Option<crate::protocol::WaitForOptions>,
1078    ) -> Result<()> {
1079        let mut params = serde_json::json!({
1080            "selector": selector,
1081            "strict": true
1082        });
1083
1084        if let Some(opts) = options {
1085            let opts_json = opts.to_json();
1086            if let Some(obj) = params.as_object_mut() {
1087                if let Some(opts_obj) = opts_json.as_object() {
1088                    obj.extend(opts_obj.clone());
1089                }
1090            }
1091        } else {
1092            // Default: wait for visible with default timeout
1093            params["state"] = serde_json::json!("visible");
1094            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1095        }
1096
1097        // waitForSelector returns an ElementHandle or null — we discard the return value
1098        let _: serde_json::Value = self.channel().send("waitForSelector", params).await?;
1099        Ok(())
1100    }
1101
1102    /// Evaluates a JavaScript expression in the scope of the element matching the selector.
1103    ///
1104    /// The element is passed as the first argument to the expression. This is equivalent
1105    /// to Playwright's `evalOnSelector` protocol call with `strict: true`.
1106    ///
1107    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1108    pub(crate) async fn locator_evaluate<T: serde::Serialize>(
1109        &self,
1110        selector: &str,
1111        expression: &str,
1112        arg: Option<T>,
1113    ) -> Result<serde_json::Value> {
1114        let serialized_arg = match arg {
1115            Some(a) => serialize_argument(&a),
1116            None => serialize_null(),
1117        };
1118
1119        let params = serde_json::json!({
1120            "selector": selector,
1121            "expression": expression,
1122            "isFunction": true,
1123            "arg": serialized_arg,
1124            "strict": true
1125        });
1126
1127        #[derive(Deserialize)]
1128        struct EvaluateResult {
1129            value: serde_json::Value,
1130        }
1131
1132        let result: EvaluateResult = self.channel().send("evalOnSelector", params).await?;
1133        Ok(parse_result(&result.value))
1134    }
1135
1136    /// Evaluates a JavaScript expression in the scope of all elements matching the selector.
1137    ///
1138    /// The array of all matching elements is passed as the first argument to the expression.
1139    /// This is equivalent to Playwright's `evalOnSelectorAll` protocol call.
1140    ///
1141    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1142    pub(crate) async fn locator_evaluate_all<T: serde::Serialize>(
1143        &self,
1144        selector: &str,
1145        expression: &str,
1146        arg: Option<T>,
1147    ) -> Result<serde_json::Value> {
1148        let serialized_arg = match arg {
1149            Some(a) => serialize_argument(&a),
1150            None => serialize_null(),
1151        };
1152
1153        let params = serde_json::json!({
1154            "selector": selector,
1155            "expression": expression,
1156            "isFunction": true,
1157            "arg": serialized_arg
1158        });
1159
1160        #[derive(Deserialize)]
1161        struct EvaluateResult {
1162            value: serde_json::Value,
1163        }
1164
1165        let result: EvaluateResult = self.channel().send("evalOnSelectorAll", params).await?;
1166        Ok(parse_result(&result.value))
1167    }
1168
1169    /// Parses a Playwright protocol array value into a Vec<String>.
1170    ///
1171    /// The Playwright protocol returns arrays as:
1172    /// `{"a": [{"s": "value1"}, {"s": "value2"}, ...]}`
1173    fn parse_string_array(value: serde_json::Value) -> Result<Vec<String>> {
1174        // Playwright protocol wraps arrays in {"a": [...]}
1175        let array = if let Some(arr) = value.get("a").and_then(|v| v.as_array()) {
1176            arr.clone()
1177        } else if let Some(arr) = value.as_array() {
1178            arr.clone()
1179        } else {
1180            return Ok(Vec::new());
1181        };
1182
1183        let mut result = Vec::with_capacity(array.len());
1184        for item in &array {
1185            // Each string item is wrapped as {"s": "value"} in Playwright protocol
1186            let s = if let Some(s) = item.get("s").and_then(|v| v.as_str()) {
1187                s.to_string()
1188            } else if let Some(s) = item.as_str() {
1189                s.to_string()
1190            } else if item.is_null() {
1191                String::new()
1192            } else {
1193                item.to_string()
1194            };
1195            result.push(s);
1196        }
1197        Ok(result)
1198    }
1199
1200    pub(crate) async fn locator_check(
1201        &self,
1202        selector: &str,
1203        options: Option<crate::protocol::CheckOptions>,
1204    ) -> Result<()> {
1205        let mut params = serde_json::json!({
1206            "selector": selector,
1207            "strict": true
1208        });
1209
1210        if let Some(opts) = options {
1211            let opts_json = opts.to_json();
1212            if let Some(obj) = params.as_object_mut() {
1213                if let Some(opts_obj) = opts_json.as_object() {
1214                    obj.extend(opts_obj.clone());
1215                }
1216            }
1217        } else {
1218            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1219        }
1220
1221        self.channel().send_no_result("check", params).await
1222    }
1223
1224    pub(crate) async fn locator_uncheck(
1225        &self,
1226        selector: &str,
1227        options: Option<crate::protocol::CheckOptions>,
1228    ) -> Result<()> {
1229        let mut params = serde_json::json!({
1230            "selector": selector,
1231            "strict": true
1232        });
1233
1234        if let Some(opts) = options {
1235            let opts_json = opts.to_json();
1236            if let Some(obj) = params.as_object_mut() {
1237                if let Some(opts_obj) = opts_json.as_object() {
1238                    obj.extend(opts_obj.clone());
1239                }
1240            }
1241        } else {
1242            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1243        }
1244
1245        self.channel().send_no_result("uncheck", params).await
1246    }
1247
1248    pub(crate) async fn locator_hover(
1249        &self,
1250        selector: &str,
1251        options: Option<crate::protocol::HoverOptions>,
1252    ) -> Result<()> {
1253        let mut params = serde_json::json!({
1254            "selector": selector,
1255            "strict": true
1256        });
1257
1258        if let Some(opts) = options {
1259            let opts_json = opts.to_json();
1260            if let Some(obj) = params.as_object_mut() {
1261                if let Some(opts_obj) = opts_json.as_object() {
1262                    obj.extend(opts_obj.clone());
1263                }
1264            }
1265        } else {
1266            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1267        }
1268
1269        self.channel().send_no_result("hover", params).await
1270    }
1271
1272    pub(crate) async fn locator_input_value(&self, selector: &str) -> Result<String> {
1273        #[derive(Deserialize)]
1274        struct InputValueResponse {
1275            value: String,
1276        }
1277
1278        let response: InputValueResponse = self
1279            .channel()
1280            .send(
1281                "inputValue",
1282                serde_json::json!({
1283                    "selector": selector,
1284                    "strict": true,
1285                    "timeout": crate::DEFAULT_TIMEOUT_MS  // Required in Playwright 1.56.1+
1286                }),
1287            )
1288            .await?;
1289
1290        Ok(response.value)
1291    }
1292
1293    pub(crate) async fn locator_select_option(
1294        &self,
1295        selector: &str,
1296        value: crate::protocol::SelectOption,
1297        options: Option<crate::protocol::SelectOptions>,
1298    ) -> Result<Vec<String>> {
1299        #[derive(Deserialize)]
1300        struct SelectOptionResponse {
1301            values: Vec<String>,
1302        }
1303
1304        let mut params = serde_json::json!({
1305            "selector": selector,
1306            "strict": true,
1307            "options": [value.to_json()]
1308        });
1309
1310        if let Some(opts) = options {
1311            let opts_json = opts.to_json();
1312            if let Some(obj) = params.as_object_mut() {
1313                if let Some(opts_obj) = opts_json.as_object() {
1314                    obj.extend(opts_obj.clone());
1315                }
1316            }
1317        } else {
1318            // No options provided, add default timeout (required in Playwright 1.56.1+)
1319            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1320        }
1321
1322        let response: SelectOptionResponse = self.channel().send("selectOption", params).await?;
1323
1324        Ok(response.values)
1325    }
1326
1327    pub(crate) async fn locator_select_option_multiple(
1328        &self,
1329        selector: &str,
1330        values: Vec<crate::protocol::SelectOption>,
1331        options: Option<crate::protocol::SelectOptions>,
1332    ) -> Result<Vec<String>> {
1333        #[derive(Deserialize)]
1334        struct SelectOptionResponse {
1335            values: Vec<String>,
1336        }
1337
1338        let values_array: Vec<_> = values.iter().map(|v| v.to_json()).collect();
1339
1340        let mut params = serde_json::json!({
1341            "selector": selector,
1342            "strict": true,
1343            "options": values_array
1344        });
1345
1346        if let Some(opts) = options {
1347            let opts_json = opts.to_json();
1348            if let Some(obj) = params.as_object_mut() {
1349                if let Some(opts_obj) = opts_json.as_object() {
1350                    obj.extend(opts_obj.clone());
1351                }
1352            }
1353        } else {
1354            // No options provided, add default timeout (required in Playwright 1.56.1+)
1355            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1356        }
1357
1358        let response: SelectOptionResponse = self.channel().send("selectOption", params).await?;
1359
1360        Ok(response.values)
1361    }
1362
1363    pub(crate) async fn locator_set_input_files(
1364        &self,
1365        selector: &str,
1366        file: &std::path::PathBuf,
1367    ) -> Result<()> {
1368        use base64::{Engine as _, engine::general_purpose};
1369        use std::io::Read;
1370
1371        // Read file contents
1372        let mut file_handle = std::fs::File::open(file)?;
1373        let mut buffer = Vec::new();
1374        file_handle.read_to_end(&mut buffer)?;
1375
1376        // Base64 encode the file contents
1377        let base64_content = general_purpose::STANDARD.encode(&buffer);
1378
1379        // Get file name
1380        let file_name = file
1381            .file_name()
1382            .and_then(|n| n.to_str())
1383            .ok_or_else(|| crate::error::Error::InvalidArgument("Invalid file path".to_string()))?;
1384
1385        self.channel()
1386            .send_no_result(
1387                "setInputFiles",
1388                serde_json::json!({
1389                    "selector": selector,
1390                    "strict": true,
1391                    "timeout": crate::DEFAULT_TIMEOUT_MS,  // Required in Playwright 1.56.1+
1392                    "payloads": [{
1393                        "name": file_name,
1394                        "buffer": base64_content
1395                    }]
1396                }),
1397            )
1398            .await
1399    }
1400
1401    pub(crate) async fn locator_set_input_files_multiple(
1402        &self,
1403        selector: &str,
1404        files: &[&std::path::PathBuf],
1405    ) -> Result<()> {
1406        use base64::{Engine as _, engine::general_purpose};
1407        use std::io::Read;
1408
1409        // If empty array, clear the files
1410        if files.is_empty() {
1411            return self
1412                .channel()
1413                .send_no_result(
1414                    "setInputFiles",
1415                    serde_json::json!({
1416                        "selector": selector,
1417                        "strict": true,
1418                        "timeout": crate::DEFAULT_TIMEOUT_MS,  // Required in Playwright 1.56.1+
1419                        "payloads": []
1420                    }),
1421                )
1422                .await;
1423        }
1424
1425        // Read and encode each file
1426        let mut file_objects = Vec::new();
1427        for file_path in files {
1428            let mut file_handle = std::fs::File::open(file_path)?;
1429            let mut buffer = Vec::new();
1430            file_handle.read_to_end(&mut buffer)?;
1431
1432            let base64_content = general_purpose::STANDARD.encode(&buffer);
1433            let file_name = file_path
1434                .file_name()
1435                .and_then(|n| n.to_str())
1436                .ok_or_else(|| {
1437                    crate::error::Error::InvalidArgument("Invalid file path".to_string())
1438                })?;
1439
1440            file_objects.push(serde_json::json!({
1441                "name": file_name,
1442                "buffer": base64_content
1443            }));
1444        }
1445
1446        self.channel()
1447            .send_no_result(
1448                "setInputFiles",
1449                serde_json::json!({
1450                    "selector": selector,
1451                    "strict": true,
1452                    "timeout": crate::DEFAULT_TIMEOUT_MS,  // Required in Playwright 1.56.1+
1453                    "payloads": file_objects
1454                }),
1455            )
1456            .await
1457    }
1458
1459    pub(crate) async fn locator_set_input_files_payload(
1460        &self,
1461        selector: &str,
1462        file: crate::protocol::FilePayload,
1463    ) -> Result<()> {
1464        use base64::{Engine as _, engine::general_purpose};
1465
1466        // Base64 encode the file contents
1467        let base64_content = general_purpose::STANDARD.encode(&file.buffer);
1468
1469        self.channel()
1470            .send_no_result(
1471                "setInputFiles",
1472                serde_json::json!({
1473                    "selector": selector,
1474                    "strict": true,
1475                    "timeout": crate::DEFAULT_TIMEOUT_MS,
1476                    "payloads": [{
1477                        "name": file.name,
1478                        "mimeType": file.mime_type,
1479                        "buffer": base64_content
1480                    }]
1481                }),
1482            )
1483            .await
1484    }
1485
1486    pub(crate) async fn locator_set_input_files_payload_multiple(
1487        &self,
1488        selector: &str,
1489        files: &[crate::protocol::FilePayload],
1490    ) -> Result<()> {
1491        use base64::{Engine as _, engine::general_purpose};
1492
1493        // If empty array, clear the files
1494        if files.is_empty() {
1495            return self
1496                .channel()
1497                .send_no_result(
1498                    "setInputFiles",
1499                    serde_json::json!({
1500                        "selector": selector,
1501                        "strict": true,
1502                        "timeout": crate::DEFAULT_TIMEOUT_MS,
1503                        "payloads": []
1504                    }),
1505                )
1506                .await;
1507        }
1508
1509        // Encode each file
1510        let file_objects: Vec<_> = files
1511            .iter()
1512            .map(|file| {
1513                let base64_content = general_purpose::STANDARD.encode(&file.buffer);
1514                serde_json::json!({
1515                    "name": file.name,
1516                    "mimeType": file.mime_type,
1517                    "buffer": base64_content
1518                })
1519            })
1520            .collect();
1521
1522        self.channel()
1523            .send_no_result(
1524                "setInputFiles",
1525                serde_json::json!({
1526                    "selector": selector,
1527                    "strict": true,
1528                    "timeout": crate::DEFAULT_TIMEOUT_MS,
1529                    "payloads": file_objects
1530                }),
1531            )
1532            .await
1533    }
1534
1535    /// Evaluates JavaScript expression in the frame context (without return value).
1536    ///
1537    /// This is used internally by Page.evaluate().
1538    pub(crate) async fn frame_evaluate_expression(&self, expression: &str) -> Result<()> {
1539        let params = serde_json::json!({
1540            "expression": expression,
1541            "arg": {
1542                "value": {"v": "null"},
1543                "handles": []
1544            }
1545        });
1546
1547        let _: serde_json::Value = self.channel().send("evaluateExpression", params).await?;
1548        Ok(())
1549    }
1550
1551    /// Evaluates JavaScript expression and returns the result as a String.
1552    ///
1553    /// The return value is automatically converted to a string representation.
1554    ///
1555    /// # Arguments
1556    ///
1557    /// * `expression` - JavaScript code to evaluate
1558    ///
1559    /// # Returns
1560    ///
1561    /// The result as a String
1562    pub(crate) async fn frame_evaluate_expression_value(&self, expression: &str) -> Result<String> {
1563        let params = serde_json::json!({
1564            "expression": expression,
1565            "arg": {
1566                "value": {"v": "null"},
1567                "handles": []
1568            }
1569        });
1570
1571        #[derive(Deserialize)]
1572        struct EvaluateResult {
1573            value: serde_json::Value,
1574        }
1575
1576        let result: EvaluateResult = self.channel().send("evaluateExpression", params).await?;
1577
1578        // Playwright protocol returns values in a wrapped format:
1579        // - String: {"s": "value"}
1580        // - Number: {"n": 123}
1581        // - Boolean: {"b": true}
1582        // - Null: {"v": "null"}
1583        // - Undefined: {"v": "undefined"}
1584        match &result.value {
1585            Value::Object(map) => {
1586                if let Some(s) = map.get("s").and_then(|v| v.as_str()) {
1587                    // String value
1588                    Ok(s.to_string())
1589                } else if let Some(n) = map.get("n") {
1590                    // Number value
1591                    Ok(n.to_string())
1592                } else if let Some(b) = map.get("b").and_then(|v| v.as_bool()) {
1593                    // Boolean value
1594                    Ok(b.to_string())
1595                } else if let Some(v) = map.get("v").and_then(|v| v.as_str()) {
1596                    // null or undefined
1597                    Ok(v.to_string())
1598                } else {
1599                    // Unknown format, return JSON
1600                    Ok(result.value.to_string())
1601                }
1602            }
1603            _ => {
1604                // Fallback for unexpected formats
1605                Ok(result.value.to_string())
1606            }
1607        }
1608    }
1609
1610    /// Evaluates a JavaScript expression in the frame context with optional arguments.
1611    ///
1612    /// Executes the provided JavaScript expression within the frame's context and returns
1613    /// the result. The return value must be JSON-serializable.
1614    ///
1615    /// # Arguments
1616    ///
1617    /// * `expression` - JavaScript code to evaluate
1618    /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1619    ///
1620    /// # Returns
1621    ///
1622    /// The result as a `serde_json::Value`
1623    ///
1624    /// # Example
1625    ///
1626    /// ```ignore
1627    /// use serde_json::json;
1628    /// use playwright_rs::protocol::Playwright;
1629    ///
1630    /// #[tokio::main]
1631    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
1632    ///     let playwright = Playwright::launch().await?;
1633    ///     let browser = playwright.chromium().launch().await?;
1634    ///     let page = browser.new_page().await?;
1635    ///     let frame = page.main_frame().await?;
1636    ///
1637    ///     // Evaluate without arguments
1638    ///     let result = frame.evaluate::<()>("1 + 1", None).await?;
1639    ///
1640    ///     // Evaluate with argument
1641    ///     let arg = json!({"x": 5, "y": 3});
1642    ///     let result = frame.evaluate::<serde_json::Value>("(arg) => arg.x + arg.y", Some(&arg)).await?;
1643    ///     assert_eq!(result, json!(8));
1644    ///     Ok(())
1645    /// }
1646    /// ```
1647    ///
1648    /// See: <https://playwright.dev/docs/api/class-frame#frame-evaluate>
1649    pub async fn evaluate<T: serde::Serialize>(
1650        &self,
1651        expression: &str,
1652        arg: Option<&T>,
1653    ) -> Result<Value> {
1654        // Serialize the argument
1655        let serialized_arg = match arg {
1656            Some(a) => serialize_argument(a),
1657            None => serialize_null(),
1658        };
1659
1660        // Build the parameters
1661        let params = serde_json::json!({
1662            "expression": expression,
1663            "arg": serialized_arg
1664        });
1665
1666        // Send the evaluateExpression command
1667        #[derive(Deserialize)]
1668        struct EvaluateResult {
1669            value: serde_json::Value,
1670        }
1671
1672        let result: EvaluateResult = self.channel().send("evaluateExpression", params).await?;
1673
1674        // Deserialize the result using parse_result
1675        Ok(parse_result(&result.value))
1676    }
1677
1678    /// Adds a `<style>` tag into the page with the desired content.
1679    ///
1680    /// # Arguments
1681    ///
1682    /// * `options` - Style tag options (content, url, or path)
1683    ///
1684    /// At least one of `content`, `url`, or `path` must be specified.
1685    ///
1686    /// # Example
1687    ///
1688    /// ```no_run
1689    /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1690    /// # #[tokio::main]
1691    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1692    /// # let playwright = Playwright::launch().await?;
1693    /// # let browser = playwright.chromium().launch().await?;
1694    /// # let context = browser.new_context().await?;
1695    /// # let page = context.new_page().await?;
1696    /// # let frame = page.main_frame().await?;
1697    /// use playwright_rs::protocol::AddStyleTagOptions;
1698    ///
1699    /// // With inline CSS
1700    /// frame.add_style_tag(
1701    ///     AddStyleTagOptions::builder()
1702    ///         .content("body { background-color: red; }")
1703    ///         .build()
1704    /// ).await?;
1705    ///
1706    /// // With URL
1707    /// frame.add_style_tag(
1708    ///     AddStyleTagOptions::builder()
1709    ///         .url("https://example.com/style.css")
1710    ///         .build()
1711    /// ).await?;
1712    /// # Ok(())
1713    /// # }
1714    /// ```
1715    ///
1716    /// See: <https://playwright.dev/docs/api/class-frame#frame-add-style-tag>
1717    pub async fn add_style_tag(
1718        &self,
1719        options: crate::protocol::page::AddStyleTagOptions,
1720    ) -> Result<Arc<crate::protocol::ElementHandle>> {
1721        // Validate that at least one option is provided
1722        options.validate()?;
1723
1724        // Build protocol parameters
1725        let mut params = serde_json::json!({});
1726
1727        if let Some(content) = &options.content {
1728            params["content"] = serde_json::json!(content);
1729        }
1730
1731        if let Some(url) = &options.url {
1732            params["url"] = serde_json::json!(url);
1733        }
1734
1735        if let Some(path) = &options.path {
1736            // Read file content and send as content
1737            let css_content = tokio::fs::read_to_string(path).await.map_err(|e| {
1738                Error::InvalidArgument(format!("Failed to read CSS file '{}': {}", path, e))
1739            })?;
1740            params["content"] = serde_json::json!(css_content);
1741        }
1742
1743        #[derive(Deserialize)]
1744        struct AddStyleTagResponse {
1745            element: serde_json::Value,
1746        }
1747
1748        let response: AddStyleTagResponse = self.channel().send("addStyleTag", params).await?;
1749
1750        let guid = response.element["guid"].as_str().ok_or_else(|| {
1751            Error::ProtocolError("Element GUID missing in addStyleTag response".to_string())
1752        })?;
1753
1754        let connection = self.base.connection();
1755        let element = connection.get_object(guid).await?;
1756
1757        let handle = element
1758            .as_any()
1759            .downcast_ref::<crate::protocol::ElementHandle>()
1760            .map(|e| Arc::new(e.clone()))
1761            .ok_or_else(|| {
1762                Error::ProtocolError(format!("Object {} is not an ElementHandle", guid))
1763            })?;
1764
1765        Ok(handle)
1766    }
1767
1768    /// Dispatches a DOM event on the element matching the selector.
1769    ///
1770    /// Unlike clicking or typing, `dispatch_event` directly sends the event without
1771    /// performing any actionability checks. It still waits for the element to be present
1772    /// in the DOM.
1773    ///
1774    /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
1775    pub(crate) async fn locator_dispatch_event(
1776        &self,
1777        selector: &str,
1778        type_: &str,
1779        event_init: Option<serde_json::Value>,
1780    ) -> Result<()> {
1781        // Serialize eventInit using Playwright's protocol argument format.
1782        // If None, use {"value": {"v": "undefined"}, "handles": []}.
1783        let event_init_serialized = match event_init {
1784            Some(v) => serialize_argument(&v),
1785            None => serde_json::json!({"value": {"v": "undefined"}, "handles": []}),
1786        };
1787
1788        let params = serde_json::json!({
1789            "selector": selector,
1790            "type": type_,
1791            "eventInit": event_init_serialized,
1792            "strict": true,
1793            "timeout": crate::DEFAULT_TIMEOUT_MS
1794        });
1795
1796        self.channel().send_no_result("dispatchEvent", params).await
1797    }
1798
1799    /// Returns the bounding box of the element matching the selector, or None if not visible.
1800    ///
1801    /// The bounding box is returned in pixels. If the element is not visible (e.g.,
1802    /// `display: none`), returns `None`.
1803    ///
1804    /// Implemented via ElementHandle because `boundingBox` is an ElementHandle-level
1805    /// protocol method, not a Frame-level method.
1806    ///
1807    /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
1808    pub(crate) async fn locator_bounding_box(
1809        &self,
1810        selector: &str,
1811    ) -> Result<Option<crate::protocol::locator::BoundingBox>> {
1812        let element = self.query_selector(selector).await?;
1813        match element {
1814            Some(handle) => handle.bounding_box().await,
1815            None => Ok(None),
1816        }
1817    }
1818
1819    /// Scrolls the element into view if it is not already visible in the viewport.
1820    ///
1821    /// Implemented via ElementHandle because `scrollIntoViewIfNeeded` is an
1822    /// ElementHandle-level protocol method, not a Frame-level method.
1823    ///
1824    /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
1825    pub(crate) async fn locator_scroll_into_view_if_needed(&self, selector: &str) -> Result<()> {
1826        let element = self.query_selector(selector).await?;
1827        match element {
1828            Some(handle) => handle.scroll_into_view_if_needed().await,
1829            None => Err(crate::error::Error::ElementNotFound(format!(
1830                "Element not found: {}",
1831                selector
1832            ))),
1833        }
1834    }
1835
1836    /// Adds a `<script>` tag into the frame with the desired content.
1837    ///
1838    /// # Arguments
1839    ///
1840    /// * `options` - Script tag options (content, url, or path)
1841    ///
1842    /// At least one of `content`, `url`, or `path` must be specified.
1843    ///
1844    /// See: <https://playwright.dev/docs/api/class-frame#frame-add-script-tag>
1845    pub async fn add_script_tag(
1846        &self,
1847        options: crate::protocol::page::AddScriptTagOptions,
1848    ) -> Result<Arc<crate::protocol::ElementHandle>> {
1849        // Validate that at least one option is provided
1850        options.validate()?;
1851
1852        // Build protocol parameters
1853        let mut params = serde_json::json!({});
1854
1855        if let Some(content) = &options.content {
1856            params["content"] = serde_json::json!(content);
1857        }
1858
1859        if let Some(url) = &options.url {
1860            params["url"] = serde_json::json!(url);
1861        }
1862
1863        if let Some(path) = &options.path {
1864            // Read file content and send as content
1865            let js_content = tokio::fs::read_to_string(path).await.map_err(|e| {
1866                Error::InvalidArgument(format!("Failed to read JS file '{}': {}", path, e))
1867            })?;
1868            params["content"] = serde_json::json!(js_content);
1869        }
1870
1871        if let Some(type_) = &options.type_ {
1872            params["type"] = serde_json::json!(type_);
1873        }
1874
1875        #[derive(Deserialize)]
1876        struct AddScriptTagResponse {
1877            element: serde_json::Value,
1878        }
1879
1880        let response: AddScriptTagResponse = self.channel().send("addScriptTag", params).await?;
1881
1882        let guid = response.element["guid"].as_str().ok_or_else(|| {
1883            Error::ProtocolError("Element GUID missing in addScriptTag response".to_string())
1884        })?;
1885
1886        let connection = self.base.connection();
1887        let element = connection.get_object(guid).await?;
1888
1889        let handle = element
1890            .as_any()
1891            .downcast_ref::<crate::protocol::ElementHandle>()
1892            .map(|e| Arc::new(e.clone()))
1893            .ok_or_else(|| {
1894                Error::ProtocolError(format!("Object {} is not an ElementHandle", guid))
1895            })?;
1896
1897        Ok(handle)
1898    }
1899}
1900
1901impl ChannelOwner for Frame {
1902    fn guid(&self) -> &str {
1903        self.base.guid()
1904    }
1905
1906    fn type_name(&self) -> &str {
1907        self.base.type_name()
1908    }
1909
1910    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
1911        self.base.parent()
1912    }
1913
1914    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
1915        self.base.connection()
1916    }
1917
1918    fn initializer(&self) -> &Value {
1919        self.base.initializer()
1920    }
1921
1922    fn channel(&self) -> &Channel {
1923        self.base.channel()
1924    }
1925
1926    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
1927        self.base.dispose(reason)
1928    }
1929
1930    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
1931        self.base.adopt(child)
1932    }
1933
1934    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
1935        self.base.add_child(guid, child)
1936    }
1937
1938    fn remove_child(&self, guid: &str) {
1939        self.base.remove_child(guid)
1940    }
1941
1942    fn on_event(&self, method: &str, params: Value) {
1943        match method {
1944            "navigated" => {
1945                // Update frame's URL when navigation occurs (including hash changes)
1946                if let Some(url_value) = params.get("url") {
1947                    if let Some(url_str) = url_value.as_str() {
1948                        // Update frame's URL
1949                        if let Ok(mut url) = self.url.write() {
1950                            *url = url_str.to_string();
1951                        }
1952                    }
1953                }
1954            }
1955            _ => {
1956                // Other events will be handled in future phases
1957                // Events: loadstate, etc.
1958            }
1959        }
1960    }
1961
1962    fn was_collected(&self) -> bool {
1963        self.base.was_collected()
1964    }
1965
1966    fn as_any(&self) -> &dyn Any {
1967        self
1968    }
1969}
1970
1971impl std::fmt::Debug for Frame {
1972    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1973        f.debug_struct("Frame").field("guid", &self.guid()).finish()
1974    }
1975}
1976
1977/// Simple glob pattern matching for URL patterns.
1978///
1979/// Supports `*` (matches any characters except `/`) and `**` (matches any characters including `/`).
1980/// This matches Playwright's URL glob pattern behavior.
1981fn glob_match(pattern: &str, text: &str) -> bool {
1982    let regex_str = pattern
1983        .replace('.', "\\.")
1984        .replace("**", "\x00") // placeholder for **
1985        .replace('*', "[^/]*")
1986        .replace('\x00', ".*"); // restore ** as .*
1987    let regex_str = format!("^{}$", regex_str);
1988    regex::Regex::new(&regex_str)
1989        .map(|re| re.is_match(text))
1990        .unwrap_or(false)
1991}