Skip to main content

headless_engine/browser/
tab.rs

1use crate::browser::builder::BrowserBuilder;
2use crate::dom::{DomTree, FormInfo, InteractiveElement, LinkInfo, PageObservation, SearchResults};
3use crate::google::{
4    GenericGoogleResult, GoogleAutocompleteResult, GoogleEndpoints, GoogleParser,
5    GoogleSearchResult,
6};
7use crate::js::context::JsRuntime;
8use crate::network::client::{FetchResult, NetworkClient};
9use crate::network::fingerprint::DeviceProfile;
10use anyhow::{Context, Result};
11use serde::{Deserialize, Serialize};
12#[derive(Debug, Serialize, Deserialize)]
13pub struct NavigationReport {
14    pub status: u16,
15    pub requested_url: String,
16    pub final_url: String,
17    pub page_title: String,
18    pub is_captcha_detected: bool,
19    pub html_bytes: usize,
20}
21
22pub struct BrowserTab {
23    network: NetworkClient,
24    dom: Option<DomTree>,
25    js: JsRuntime,
26    pub current_url: Option<String>,
27}
28
29impl BrowserTab {
30    pub fn new() -> Result<Self> {
31        Self::with_profile(DeviceProfile::ChromeWindows)
32    }
33
34    pub fn builder() -> BrowserBuilder {
35        BrowserBuilder::new()
36    }
37
38    pub fn with_profile(profile: DeviceProfile) -> Result<Self> {
39        let network = NetworkClient::with_profile(profile)?;
40        Self::from_network(network)
41    }
42
43    pub fn from_network(network: NetworkClient) -> Result<Self> {
44        let js = JsRuntime::with_fingerprint(&network.fingerprint)?;
45        Ok(Self {
46            network,
47            dom: None,
48            js,
49            current_url: None,
50        })
51    }
52
53    pub fn profile(&self) -> DeviceProfile {
54        self.network.profile
55    }
56
57    pub fn set_profile(&mut self, profile: DeviceProfile) -> Result<()> {
58        self.network.set_profile(profile)?;
59        self.js = JsRuntime::with_fingerprint(&self.network.fingerprint)?;
60        Ok(())
61    }
62
63    pub async fn navigate(&mut self, url: &str) -> Result<NavigationReport> {
64        let fetch_result: FetchResult = self.network.fetch(url).await?;
65        let dom = DomTree::parse(&fetch_result.html)?;
66
67        let search_results = dom.parse_google_search_results();
68        let page_title = search_results.page_title.clone();
69        let html_bytes = fetch_result.html.len();
70
71        let _ = self
72            .js
73            .update_page_state(&fetch_result.final_url, &page_title);
74
75        self.dom = Some(dom);
76        self.current_url = Some(fetch_result.final_url.clone());
77
78        Ok(NavigationReport {
79            status: fetch_result.status,
80            requested_url: url.to_string(),
81            final_url: fetch_result.final_url,
82            page_title,
83            is_captcha_detected: fetch_result.is_captcha_detected,
84            html_bytes,
85        })
86    }
87
88    /// Default Google Search with automated query encoding and mode routing
89    pub async fn search(&mut self, query: &str) -> Result<NavigationReport> {
90        self.search_google(query, None).await
91    }
92
93    /// Search Google with specific query modes (e.g. "ai" for udm=50, "web" for udm=14, "images" for udm=2, "news")
94    pub async fn search_google(
95        &mut self,
96        query: &str,
97        mode: Option<&str>,
98    ) -> Result<NavigationReport> {
99        let encoded: String = query
100            .chars()
101            .map(|c| match c {
102                ' ' => "+".to_string(),
103                'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
104                _ => format!("%{:02X}", c as u8),
105            })
106            .collect();
107
108        let url = match mode {
109            Some("ai") | Some("udm50") => {
110                format!("https://www.google.com/search?q={}&udm=50", encoded)
111            }
112            Some("web") | Some("udm14") => {
113                format!("https://www.google.com/search?q={}&udm=14", encoded)
114            }
115            Some("images") | Some("udm2") => {
116                format!("https://www.google.com/search?q={}&udm=2", encoded)
117            }
118            Some("news") => format!("https://www.google.com/search?q={}&tbm=nws", encoded),
119            _ => format!("https://www.google.com/search?q={}", encoded),
120        };
121        self.navigate(&url).await
122    }
123
124    pub async fn google_search(&mut self, query: &str) -> Result<GoogleSearchResult> {
125        let url = GoogleEndpoints::search(query);
126        let nav = self.navigate(&url).await?;
127        let html = self
128            .dom
129            .as_ref()
130            .map(|d| d.raw_content.clone())
131            .unwrap_or_default();
132        Ok(GoogleParser::parse_search_results(&html, &nav.final_url))
133    }
134
135    pub async fn google_web_search(&mut self, query: &str) -> Result<GoogleSearchResult> {
136        let url = GoogleEndpoints::web_search(query);
137        let nav = self.navigate(&url).await?;
138        let html = self
139            .dom
140            .as_ref()
141            .map(|d| d.raw_content.clone())
142            .unwrap_or_default();
143        Ok(GoogleParser::parse_search_results(&html, &nav.final_url))
144    }
145
146    pub async fn google_image_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
147        let url = GoogleEndpoints::image_search(query);
148        let nav = self.navigate(&url).await?;
149        let html = self
150            .dom
151            .as_ref()
152            .map(|d| d.raw_content.clone())
153            .unwrap_or_default();
154        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
155    }
156
157    pub async fn google_video_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
158        let url = GoogleEndpoints::video_search(query);
159        let nav = self.navigate(&url).await?;
160        let html = self
161            .dom
162            .as_ref()
163            .map(|d| d.raw_content.clone())
164            .unwrap_or_default();
165        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
166    }
167
168    pub async fn google_short_video_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
169        let url = GoogleEndpoints::short_video_search(query);
170        let nav = self.navigate(&url).await?;
171        let html = self
172            .dom
173            .as_ref()
174            .map(|d| d.raw_content.clone())
175            .unwrap_or_default();
176        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
177    }
178
179    pub async fn google_news_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
180        let url = GoogleEndpoints::news_search(query);
181        let nav = self.navigate(&url).await?;
182        let html = self
183            .dom
184            .as_ref()
185            .map(|d| d.raw_content.clone())
186            .unwrap_or_default();
187        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
188    }
189
190    pub async fn google_forum_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
191        let url = GoogleEndpoints::forum_search(query);
192        let nav = self.navigate(&url).await?;
193        let html = self
194            .dom
195            .as_ref()
196            .map(|d| d.raw_content.clone())
197            .unwrap_or_default();
198        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
199    }
200
201    pub async fn google_shopping_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
202        let url = GoogleEndpoints::shopping_search(query);
203        let nav = self.navigate(&url).await?;
204        let html = self
205            .dom
206            .as_ref()
207            .map(|d| d.raw_content.clone())
208            .unwrap_or_default();
209        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
210    }
211
212    pub async fn google_product_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
213        let url = GoogleEndpoints::product_search(query);
214        let nav = self.navigate(&url).await?;
215        let html = self
216            .dom
217            .as_ref()
218            .map(|d| d.raw_content.clone())
219            .unwrap_or_default();
220        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
221    }
222
223    pub async fn google_books_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
224        let url = GoogleEndpoints::books_search(query);
225        let nav = self.navigate(&url).await?;
226        let html = self
227            .dom
228            .as_ref()
229            .map(|d| d.raw_content.clone())
230            .unwrap_or_default();
231        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
232    }
233
234    pub async fn google_autocomplete(&mut self, query: &str) -> Result<GoogleAutocompleteResult> {
235        let url = GoogleEndpoints::autocomplete(query);
236        let fetch_result = self.network.fetch(&url).await?;
237        Ok(GoogleParser::parse_autocomplete(&fetch_result.html))
238    }
239
240    pub async fn google_ai_overview(&mut self, query: &str) -> Result<GenericGoogleResult> {
241        let url = GoogleEndpoints::ai_overview(query);
242        let nav = self.navigate(&url).await?;
243        let html = self
244            .dom
245            .as_ref()
246            .map(|d| d.raw_content.clone())
247            .unwrap_or_default();
248        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
249    }
250
251    pub async fn google_ai_mode(&mut self, query: &str) -> Result<GenericGoogleResult> {
252        let url = GoogleEndpoints::ai_mode(query);
253        let nav = self.navigate(&url).await?;
254        let html = self
255            .dom
256            .as_ref()
257            .map(|d| d.raw_content.clone())
258            .unwrap_or_default();
259        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
260    }
261
262    pub async fn google_scholar_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
263        let url = GoogleEndpoints::scholar_search(query);
264        let nav = self.navigate(&url).await?;
265        let html = self
266            .dom
267            .as_ref()
268            .map(|d| d.raw_content.clone())
269            .unwrap_or_default();
270        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
271    }
272
273    pub async fn google_patents_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
274        let url = GoogleEndpoints::patents_search(query);
275        let nav = self.navigate(&url).await?;
276        let html = self
277            .dom
278            .as_ref()
279            .map(|d| d.raw_content.clone())
280            .unwrap_or_default();
281        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
282    }
283
284    pub async fn google_maps_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
285        let url = GoogleEndpoints::maps_search(query);
286        let nav = self.navigate(&url).await?;
287        let html = self
288            .dom
289            .as_ref()
290            .map(|d| d.raw_content.clone())
291            .unwrap_or_default();
292        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
293    }
294
295    pub async fn google_finance_quote(&mut self, ticker: &str) -> Result<GenericGoogleResult> {
296        let url = GoogleEndpoints::finance_quote(ticker);
297        let nav = self.navigate(&url).await?;
298        let html = self
299            .dom
300            .as_ref()
301            .map(|d| d.raw_content.clone())
302            .unwrap_or_default();
303        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
304    }
305
306    pub async fn google_trends_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
307        let url = GoogleEndpoints::trends_search(query);
308        let nav = self.navigate(&url).await?;
309        let html = self
310            .dom
311            .as_ref()
312            .map(|d| d.raw_content.clone())
313            .unwrap_or_default();
314        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
315    }
316
317    pub async fn google_flights_search(
318        &mut self,
319        origin: &str,
320        dest: &str,
321    ) -> Result<GenericGoogleResult> {
322        let url = GoogleEndpoints::flights_search(origin, dest);
323        let nav = self.navigate(&url).await?;
324        let html = self
325            .dom
326            .as_ref()
327            .map(|d| d.raw_content.clone())
328            .unwrap_or_default();
329        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
330    }
331
332    pub async fn google_hotels_search(&mut self, location: &str) -> Result<GenericGoogleResult> {
333        let url = GoogleEndpoints::hotels_search(location);
334        let nav = self.navigate(&url).await?;
335        let html = self
336            .dom
337            .as_ref()
338            .map(|d| d.raw_content.clone())
339            .unwrap_or_default();
340        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
341    }
342
343    pub async fn google_travel_explore(
344        &mut self,
345        destination: &str,
346    ) -> Result<GenericGoogleResult> {
347        let url = GoogleEndpoints::travel_explore(destination);
348        let nav = self.navigate(&url).await?;
349        let html = self
350            .dom
351            .as_ref()
352            .map(|d| d.raw_content.clone())
353            .unwrap_or_default();
354        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
355    }
356
357    pub async fn youtube_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
358        let url = GoogleEndpoints::youtube_search(query);
359        let nav = self.navigate(&url).await?;
360        let html = self
361            .dom
362            .as_ref()
363            .map(|d| d.raw_content.clone())
364            .unwrap_or_default();
365        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
366    }
367
368    pub async fn youtube_shorts_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
369        let url = GoogleEndpoints::youtube_shorts_search(query);
370        let nav = self.navigate(&url).await?;
371        let html = self
372            .dom
373            .as_ref()
374            .map(|d| d.raw_content.clone())
375            .unwrap_or_default();
376        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
377    }
378
379    pub async fn youtube_video(&mut self, video_id: &str) -> Result<GenericGoogleResult> {
380        let url = GoogleEndpoints::youtube_video(video_id);
381        let nav = self.navigate(&url).await?;
382        let html = self
383            .dom
384            .as_ref()
385            .map(|d| d.raw_content.clone())
386            .unwrap_or_default();
387        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
388    }
389
390    pub async fn youtube_channel(&mut self, channel: &str) -> Result<GenericGoogleResult> {
391        let url = GoogleEndpoints::youtube_channel(channel);
392        let nav = self.navigate(&url).await?;
393        let html = self
394            .dom
395            .as_ref()
396            .map(|d| d.raw_content.clone())
397            .unwrap_or_default();
398        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
399    }
400
401    pub async fn youtube_playlist(&mut self, playlist_id: &str) -> Result<GenericGoogleResult> {
402        let url = GoogleEndpoints::youtube_playlist(playlist_id);
403        let nav = self.navigate(&url).await?;
404        let html = self
405            .dom
406            .as_ref()
407            .map(|d| d.raw_content.clone())
408            .unwrap_or_default();
409        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
410    }
411
412    pub async fn google_lens_visual_matches(
413        &mut self,
414        image_url: &str,
415    ) -> Result<GenericGoogleResult> {
416        let url = GoogleEndpoints::lens_visual_matches(image_url);
417        let nav = self.navigate(&url).await?;
418        let html = self
419            .dom
420            .as_ref()
421            .map(|d| d.raw_content.clone())
422            .unwrap_or_default();
423        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
424    }
425
426    pub async fn google_lens_exact_matches(
427        &mut self,
428        image_url: &str,
429    ) -> Result<GenericGoogleResult> {
430        let url = GoogleEndpoints::lens_exact_matches(image_url);
431        let nav = self.navigate(&url).await?;
432        let html = self
433            .dom
434            .as_ref()
435            .map(|d| d.raw_content.clone())
436            .unwrap_or_default();
437        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
438    }
439
440    pub async fn google_lens_products(&mut self, image_url: &str) -> Result<GenericGoogleResult> {
441        let url = GoogleEndpoints::lens_products(image_url);
442        let nav = self.navigate(&url).await?;
443        let html = self
444            .dom
445            .as_ref()
446            .map(|d| d.raw_content.clone())
447            .unwrap_or_default();
448        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
449    }
450
451    pub async fn google_lens_about_image(
452        &mut self,
453        image_url: &str,
454    ) -> Result<GenericGoogleResult> {
455        let url = GoogleEndpoints::lens_about_image(image_url);
456        let nav = self.navigate(&url).await?;
457        let html = self
458            .dom
459            .as_ref()
460            .map(|d| d.raw_content.clone())
461            .unwrap_or_default();
462        Ok(GoogleParser::parse_generic(&html, &nav.final_url))
463    }
464
465    pub fn google_capabilities(&self) -> Vec<&'static str> {
466        vec![
467            "google_search",
468            "google_web_search",
469            "google_image_search",
470            "google_video_search",
471            "google_short_video_search",
472            "google_news_search",
473            "google_forum_search",
474            "google_shopping_search",
475            "google_product_search",
476            "google_books_search",
477            "google_autocomplete",
478            "google_ai_overview",
479            "google_ai_mode",
480            "google_scholar_search",
481            "google_patents_search",
482            "google_maps_search",
483            "google_finance_quote",
484            "google_trends_search",
485            "google_flights_search",
486            "google_hotels_search",
487            "google_travel_explore",
488            "youtube_search",
489            "youtube_shorts_search",
490            "youtube_video",
491            "youtube_channel",
492            "youtube_playlist",
493            "google_lens_visual_matches",
494            "google_lens_exact_matches",
495            "google_lens_products",
496            "google_lens_about_image",
497            "google_capabilities",
498        ]
499    }
500
501    pub fn set_content(&mut self, html: &str, url: Option<&str>) -> Result<NavigationReport> {
502        let dom = DomTree::parse(html)?;
503        let search_results = dom.parse_google_search_results();
504        let page_title = search_results.page_title.clone();
505        let final_url = url.unwrap_or("about:blank").to_string();
506        let html_bytes = html.len();
507
508        let _ = self.js.update_page_state(&final_url, &page_title);
509
510        self.dom = Some(dom);
511        self.current_url = Some(final_url.clone());
512
513        Ok(NavigationReport {
514            status: 200,
515            requested_url: final_url.clone(),
516            final_url,
517            page_title,
518            is_captcha_detected: search_results.is_captcha_detected,
519            html_bytes,
520        })
521    }
522
523    pub fn observe(&self) -> Option<PageObservation> {
524        let dom = self.dom.as_ref()?;
525        let url = self.current_url.clone().unwrap_or_default();
526        let results = dom.parse_google_search_results();
527        let elements = dom.extract_interactive_elements(Some(&url));
528
529        let mut tree_lines = Vec::new();
530        for el in &elements {
531            tree_lines.push(el.to_agent_string());
532        }
533
534        let agent_tree_text = tree_lines.join("\n");
535        let content_summary_markdown = dom.extract_markdown(None, Some(&url));
536
537        Some(PageObservation {
538            url,
539            title: results.page_title,
540            is_captcha_detected: results.is_captcha_detected,
541            interactive_elements: elements,
542            agent_tree_text,
543            content_summary_markdown,
544        })
545    }
546
547    pub fn evaluate_js(&mut self, code: &str) -> Result<String> {
548        self.js.evaluate(code)
549    }
550
551    pub fn extract_dom(&self, selector: Option<&str>) -> Option<String> {
552        self.dom.as_ref().and_then(|d| d.extract(selector))
553    }
554
555    pub fn extract_markdown(&self, selector: Option<&str>) -> Option<String> {
556        self.dom
557            .as_ref()
558            .map(|d| d.extract_markdown(selector, self.current_url.as_deref()))
559    }
560
561    pub fn extract_interactive_elements(&self) -> Vec<InteractiveElement> {
562        self.dom
563            .as_ref()
564            .map(|d| d.extract_interactive_elements(self.current_url.as_deref()))
565            .unwrap_or_default()
566    }
567
568    pub fn extract_links(&self) -> Vec<LinkInfo> {
569        self.dom
570            .as_ref()
571            .map(|d| d.extract_links(self.current_url.as_deref()))
572            .unwrap_or_default()
573    }
574
575    pub fn extract_forms(&self) -> Vec<FormInfo> {
576        self.dom
577            .as_ref()
578            .map(|d| d.extract_forms())
579            .unwrap_or_default()
580    }
581
582    pub fn extract_search_results(&self) -> Option<SearchResults> {
583        self.dom.as_ref().map(|d| d.parse_google_search_results())
584    }
585
586    pub async fn screenshot_async(&self) -> Option<crate::dom::ScreenshotResult> {
587        let dom = self.dom.as_ref()?;
588        let url = self.current_url.as_deref().unwrap_or("about:blank");
589        let results = dom.parse_google_search_results();
590        Some(
591            dom.screenshot_async(url, &results.page_title, self.current_url.as_deref())
592                .await,
593        )
594    }
595
596    pub fn screenshot(&self) -> Option<crate::dom::ScreenshotResult> {
597        let dom = self.dom.as_ref()?;
598        let url = self.current_url.as_deref().unwrap_or("about:blank");
599        let results = dom.parse_google_search_results();
600        Some(dom.screenshot(url, &results.page_title, self.current_url.as_deref()))
601    }
602
603    pub fn screenshot_svg(&self) -> Option<String> {
604        self.screenshot().map(|s| s.svg)
605    }
606
607    pub fn screenshot_layout(&self) -> Option<String> {
608        self.screenshot().map(|s| s.layout_wireframe)
609    }
610
611    pub async fn act_click(&mut self, target: &str) -> Result<Option<NavigationReport>> {
612        // Check if target is a numerical index
613        if let Ok(idx) = target.parse::<usize>() {
614            let elements = self.extract_interactive_elements();
615            if let Some(el) = elements.iter().find(|e| e.index == idx) {
616                if !el.href.is_empty() {
617                    let report = self.navigate(&el.href).await?;
618                    return Ok(Some(report));
619                }
620                return self.click(&el.selector).await;
621            }
622        }
623
624        self.click(target).await
625    }
626
627    pub async fn act_type(&mut self, target: &str, text: &str) -> Result<String> {
628        // Check if target is a numerical index
629        if let Ok(idx) = target.parse::<usize>() {
630            let elements = self.extract_interactive_elements();
631            if let Some(el) = elements.iter().find(|e| e.index == idx) {
632                return self.type_text(&el.selector, text);
633            }
634        }
635
636        self.type_text(target, text)
637    }
638
639    pub async fn click(&mut self, selector_or_text: &str) -> Result<Option<NavigationReport>> {
640        let links = self.extract_links();
641
642        // 1. Check if matches href or anchor text
643        if let Some(link) = links.iter().find(|l| {
644            l.text.eq_ignore_ascii_case(selector_or_text)
645                || l.href.contains(selector_or_text)
646                || l.text
647                    .to_lowercase()
648                    .contains(&selector_or_text.to_lowercase())
649        }) {
650            let report = self.navigate(&link.href).await?;
651            return Ok(Some(report));
652        }
653
654        // 2. Try evaluating JS click
655        let sel_json = serde_json::to_string(selector_or_text)
656            .unwrap_or_else(|_| format!("\"{}\"", selector_or_text));
657        let js_code = format!(
658            "var el = document.querySelector({}); if (el) {{ try {{ el.click(); }} catch(e){{}} true; }} else {{ false; }}",
659            sel_json
660        );
661        let _ = self.evaluate_js(&js_code);
662
663        Ok(None)
664    }
665
666    pub fn type_text(&mut self, selector: &str, text: &str) -> Result<String> {
667        let sel_json =
668            serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
669        let text_json = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
670        let js_code = format!(
671            r#"var el = document.querySelector({sel});
672            if (el) {{
673                if ('value' in el) {{
674                    el.value = {txt};
675                }} else {{
676                    el.innerText = {txt};
677                    el.textContent = {txt};
678                }}
679                try {{
680                    el.dispatchEvent(new Event('input', {{ bubbles: true }}));
681                    el.dispatchEvent(new Event('change', {{ bubbles: true }}));
682                }} catch(e) {{}}
683                'updated';
684            }} else {{
685                'not_found';
686            }}"#,
687            sel = sel_json,
688            txt = text_json
689        );
690        self.evaluate_js(&js_code)
691            .context("Failed to evaluate type action")
692    }
693}