1use std::fmt::{Display, Formatter};
2use std::sync::{Arc, Mutex, OnceLock};
3
4use crate::proto::browser_session_command::Command as BrowserCommand;
5use crate::proto::browser_session_event::Event as BrowserEvent;
6use crate::proto::engine_service_client::EngineServiceClient;
7use crate::proto::tab_session_command::Command as TabCommand;
8use crate::proto::tab_session_event::Event as TabEvent;
9use crate::proto::{
10 BrowserKind as ProtoBrowserKind, BrowserLaunchedEvent, BrowserSessionCommand,
11 BrowserSessionEvent, ClickElementCommand, CloseBrowserSessionCommand, CloseTabSessionCommand,
12 CommandRetryOptions, CountElementsCommand, ElementCountedEvent, ElementsHighlightedEvent,
13 FillElementCommand, FocusElementCommand, GetInnerTextCommand, GetTextContentCommand,
14 HighlightElementsCommand, HoverElementCommand, LaunchBrowserCommand, NavigateTabCommand,
15 OpenTabCommand, PingRequest, PressKeyCommand, SessionPingCommand,
16 TabSessionCommand, TabSessionEvent, TabSessionPingCommand, WaitForSelectorCommand,
17};
18use tokio::sync::{Mutex as AsyncMutex, mpsc};
19use tokio_stream::wrappers::ReceiverStream;
20use tonic::transport::Channel;
21
22const DEFAULT_SERVER_ADDR: &str = "http://127.0.0.1:50051";
23const SERVER_ADDR_ENV_VAR: &str = "ALLWRIGHT_SERVER_ADDR";
24
25type Result<T> = std::result::Result<T, Error>;
26
27static RUNTIME: OnceLock<Mutex<Option<Arc<RuntimeClient>>>> = OnceLock::new();
28static SERVER_ADDR_OVERRIDE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
29
30#[derive(Debug)]
31pub struct Error {
32 message: String,
33}
34
35impl Error {
36 fn new(message: impl Into<String>) -> Self {
37 Self {
38 message: message.into(),
39 }
40 }
41}
42
43impl Display for Error {
44 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45 f.write_str(&self.message)
46 }
47}
48
49impl std::error::Error for Error {}
50
51impl From<tonic::transport::Error> for Error {
52 fn from(value: tonic::transport::Error) -> Self {
53 Self::new(format!("transport error: {value}"))
54 }
55}
56
57impl From<tonic::Status> for Error {
58 fn from(value: tonic::Status) -> Self {
59 Self::new(format!("grpc status error: {value}"))
60 }
61}
62
63#[derive(Debug, Clone, Default)]
64pub struct LaunchOptions {
65 pub browser_binary: Option<String>,
66 pub timeout_ms: Option<u32>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum BrowserKind {
71 Chromium,
72 Firefox,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct BrowserType {
77 browser_kind: BrowserKind,
78}
79
80#[derive(Debug, Clone, Default)]
81pub struct CommandOptions {
82 pub timeout_ms: Option<u32>,
83}
84
85#[derive(Debug, Clone)]
86pub struct NavigateResult {
87 pub url: String,
88 pub note: String,
89 pub bidi_session_id: String,
90 pub mapper_target_id: String,
91 pub mapper_session_id: String,
92 pub package_version: String,
93}
94
95#[derive(Debug, Clone)]
96pub struct ClickResult {
97 pub selector: String,
98 pub note: String,
99 pub bidi_session_id: String,
100}
101
102#[derive(Debug, Clone)]
103pub struct CountResult {
104 pub selector: String,
105 pub count: u32,
106 pub note: String,
107}
108
109#[derive(Debug, Clone, Default)]
110pub struct HighlightOptions {
111 pub timeout_ms: Option<u32>,
112 pub duration_ms: Option<u32>,
113}
114
115#[derive(Debug, Clone)]
116pub struct HighlightResult {
117 pub selector: String,
118 pub count: u32,
119 pub note: String,
120}
121
122#[derive(Debug, Clone)]
123pub struct ElementResult {
124 pub selector: String,
125 pub note: String,
126}
127
128#[derive(Debug, Clone)]
129pub struct FillResult {
130 pub selector: String,
131 pub value: String,
132 pub note: String,
133}
134
135#[derive(Debug, Clone)]
136pub struct PressResult {
137 pub selector: String,
138 pub key: String,
139 pub note: String,
140}
141
142#[derive(Debug, Clone)]
143pub struct TextResult {
144 pub selector: String,
145 pub text: String,
146 pub note: String,
147}
148
149#[derive(Debug, Clone, Default)]
150pub struct PressOptions {
151 pub timeout_ms: Option<u32>,
152 pub text: Option<String>,
153}
154
155#[derive(Debug, Clone, Default)]
156pub struct WaitForSelectorOptions {
157 pub timeout_ms: Option<u32>,
158 pub visible: Option<bool>,
159}
160
161#[derive(Debug, Clone)]
162pub struct WaitForSelectorResult {
163 pub selector: String,
164 pub visible: bool,
165 pub note: String,
166}
167
168#[derive(Clone)]
169pub struct Browser {
170 inner: Arc<BrowserInner>,
171}
172
173#[derive(Clone)]
174pub struct Tab {
175 inner: Arc<TabInner>,
176}
177
178pub type Page = Tab;
179
180#[derive(Clone)]
181pub struct Locator {
182 page: Tab,
183 selector: String,
184}
185
186#[derive(Clone)]
187struct RuntimeClient {
188 engine: EngineServiceClient<Channel>,
189}
190
191struct BrowserInner {
192 runtime: Arc<RuntimeClient>,
193 state: AsyncMutex<BrowserState>,
194 session_id: String,
195 browser_name: String,
196 launch_note: String,
197 cdp_websocket_url: String,
198 user_data_dir: String,
199 initial_tab: Tab,
200}
201
202struct BrowserState {
203 command_tx: mpsc::Sender<BrowserSessionCommand>,
204 events: tonic::Streaming<BrowserSessionEvent>,
205 closed: bool,
206}
207
208struct TabInner {
209 runtime: Arc<RuntimeClient>,
210 browser_session_id: String,
211 session_id: String,
212 state: AsyncMutex<TabState>,
213}
214
215#[derive(Default)]
216struct TabState {
217 handle: Option<TabHandle>,
218}
219
220struct TabHandle {
221 command_tx: mpsc::Sender<TabSessionCommand>,
222 events: tonic::Streaming<TabSessionEvent>,
223 closed: bool,
224}
225
226pub async fn ping() -> Result<String> {
227 let runtime = get_runtime().await?;
228 let mut engine = runtime.engine.clone();
229 let response = engine.ping(tonic::Request::new(PingRequest {})).await?;
230 Ok(response.into_inner().message)
231}
232
233pub async fn launch_chrome(options: LaunchOptions) -> Result<Browser> {
234 launch_browser(BrowserKind::Chromium, options).await
235}
236
237pub async fn launch_firefox(options: LaunchOptions) -> Result<Browser> {
238 launch_browser(BrowserKind::Firefox, options).await
239}
240
241pub async fn launch_browser(browser_kind: BrowserKind, options: LaunchOptions) -> Result<Browser> {
242 let runtime = get_runtime().await?;
243 let mut engine = runtime.engine.clone();
244 let (command_tx, command_rx) = mpsc::channel(16);
245 let response = engine
246 .browser_session(tonic::Request::new(ReceiverStream::new(command_rx)))
247 .await?;
248 let mut events = response.into_inner();
249
250 command_tx
251 .send(BrowserSessionCommand {
252 command: Some(BrowserCommand::LaunchBrowser(LaunchBrowserCommand {
253 browser_kind: match browser_kind {
254 BrowserKind::Chromium => ProtoBrowserKind::Chromium as i32,
255 BrowserKind::Firefox => ProtoBrowserKind::Firefox as i32,
256 },
257 browser_binary: options.browser_binary,
258 retry_options: command_retry_options(options.timeout_ms),
259 })),
260 })
261 .await
262 .map_err(|_| Error::new("failed to send LaunchBrowserCommand to browser session"))?;
263
264 loop {
265 let event = events
266 .message()
267 .await?
268 .ok_or_else(|| Error::new("browser session closed before launch response"))?;
269
270 match event.event {
271 Some(BrowserEvent::BrowserLaunched(BrowserLaunchedEvent {
272 browser,
273 note,
274 user_data_dir,
275 initial_tab_session_id,
276 ..
277 })) => {
278 let browser_session_id = event.session_id;
279 let initial_tab = Tab {
280 inner: Arc::new(TabInner {
281 runtime: Arc::clone(&runtime),
282 browser_session_id: browser_session_id.clone(),
283 session_id: initial_tab_session_id,
284 state: AsyncMutex::new(TabState::default()),
285 }),
286 };
287 return Ok(Browser {
288 inner: Arc::new(BrowserInner {
289 runtime,
290 state: AsyncMutex::new(BrowserState {
291 command_tx,
292 events,
293 closed: false,
294 }),
295 session_id: browser_session_id,
296 browser_name: browser,
297 launch_note: note,
298 cdp_websocket_url: String::new(),
299 user_data_dir,
300 initial_tab,
301 }),
302 });
303 }
304 Some(BrowserEvent::ChromeLaunched(launched)) => {
305 let browser_session_id = event.session_id;
306 let initial_tab = Tab {
307 inner: Arc::new(TabInner {
308 runtime: Arc::clone(&runtime),
309 browser_session_id: browser_session_id.clone(),
310 session_id: launched.initial_tab_session_id.clone(),
311 state: AsyncMutex::new(TabState::default()),
312 }),
313 };
314 return Ok(Browser {
315 inner: Arc::new(BrowserInner {
316 runtime,
317 state: AsyncMutex::new(BrowserState {
318 command_tx,
319 events,
320 closed: false,
321 }),
322 session_id: browser_session_id,
323 browser_name: launched.browser,
324 launch_note: launched.note,
325 cdp_websocket_url: launched.cdp_websocket_url,
326 user_data_dir: launched.user_data_dir,
327 initial_tab,
328 }),
329 });
330 }
331 Some(BrowserEvent::Error(error)) => {
332 return Err(Error::new(format!(
333 "browser session error during launch: {}",
334 error.message
335 )));
336 }
337 _ => {}
338 }
339 }
340}
341
342pub fn chromium() -> BrowserType {
343 BrowserType {
344 browser_kind: BrowserKind::Chromium,
345 }
346}
347
348pub fn firefox() -> BrowserType {
349 BrowserType {
350 browser_kind: BrowserKind::Firefox,
351 }
352}
353
354pub fn set_server_addr(server_addr: impl Into<String>) -> Result<()> {
355 let normalized = normalize_server_addr(&server_addr.into());
356 let mut override_slot = server_addr_override_slot()
357 .lock()
358 .map_err(|_| Error::new("server address override lock is poisoned"))?;
359 *override_slot = Some(normalized);
360 drop(override_slot);
361
362 let mut runtime = runtime_slot()
363 .lock()
364 .map_err(|_| Error::new("runtime singleton lock is poisoned"))?;
365 *runtime = None;
366 Ok(())
367}
368
369pub async fn shutdown() {
370 if let Ok(mut runtime) = runtime_slot().lock() {
371 *runtime = None;
372 }
373}
374
375impl Browser {
376 pub fn page(&self) -> Page {
377 self.initial_tab()
378 }
379
380 pub fn initial_page(&self) -> Page {
381 self.initial_tab()
382 }
383
384 pub fn session_id(&self) -> &str {
385 &self.inner.session_id
386 }
387
388 pub fn browser_name(&self) -> &str {
389 &self.inner.browser_name
390 }
391
392 pub fn launch_note(&self) -> &str {
393 &self.inner.launch_note
394 }
395
396 pub fn cdp_websocket_url(&self) -> &str {
397 &self.inner.cdp_websocket_url
398 }
399
400 pub fn user_data_dir(&self) -> &str {
401 &self.inner.user_data_dir
402 }
403
404 pub fn initial_tab(&self) -> Tab {
405 self.inner.initial_tab.clone()
406 }
407
408 pub async fn new_tab(&self) -> Result<Tab> {
409 self.new_tab_with_options(CommandOptions::default()).await
410 }
411
412 pub async fn new_page(&self) -> Result<Page> {
413 self.new_tab().await
414 }
415
416 pub async fn new_tab_with_options(&self, options: CommandOptions) -> Result<Tab> {
417 let mut state = self.inner.state.lock().await;
418 if state.closed {
419 return Err(Error::new(format!(
420 "browser session {} is closed",
421 self.inner.session_id
422 )));
423 }
424
425 state
426 .command_tx
427 .send(BrowserSessionCommand {
428 command: Some(BrowserCommand::OpenTab(OpenTabCommand {
429 retry_options: command_retry_options(options.timeout_ms),
430 })),
431 })
432 .await
433 .map_err(|_| Error::new("failed to send OpenTabCommand to browser session"))?;
434
435 loop {
436 let event =
437 state.events.message().await?.ok_or_else(|| {
438 Error::new("browser session closed while waiting for new tab")
439 })?;
440
441 match event.event {
442 Some(BrowserEvent::TabOpened(opened)) => {
443 return Ok(Tab {
444 inner: Arc::new(TabInner {
445 runtime: Arc::clone(&self.inner.runtime),
446 browser_session_id: self.inner.session_id.clone(),
447 session_id: opened.tab_session_id,
448 state: AsyncMutex::new(TabState::default()),
449 }),
450 });
451 }
452 Some(BrowserEvent::Error(error)) => {
453 return Err(Error::new(format!(
454 "browser session error while opening tab: {}",
455 error.message
456 )));
457 }
458 _ => {}
459 }
460 }
461 }
462
463 pub async fn ping(&self, message: impl Into<String>) -> Result<String> {
464 let mut state = self.inner.state.lock().await;
465 if state.closed {
466 return Err(Error::new(format!(
467 "browser session {} is closed",
468 self.inner.session_id
469 )));
470 }
471
472 state
473 .command_tx
474 .send(BrowserSessionCommand {
475 command: Some(BrowserCommand::Ping(SessionPingCommand {
476 message: message.into(),
477 })),
478 })
479 .await
480 .map_err(|_| Error::new("failed to send SessionPingCommand to browser session"))?;
481
482 loop {
483 let event = state
484 .events
485 .message()
486 .await?
487 .ok_or_else(|| Error::new("browser session closed while waiting for pong"))?;
488
489 match event.event {
490 Some(BrowserEvent::Pong(pong)) => return Ok(pong.message),
491 Some(BrowserEvent::Error(error)) => {
492 return Err(Error::new(format!(
493 "browser session error while pinging: {}",
494 error.message
495 )));
496 }
497 _ => {}
498 }
499 }
500 }
501
502 pub async fn close(&self) -> Result<()> {
503 let mut state = self.inner.state.lock().await;
504 if state.closed {
505 return Ok(());
506 }
507
508 state
509 .command_tx
510 .send(BrowserSessionCommand {
511 command: Some(BrowserCommand::Close(CloseBrowserSessionCommand {})),
512 })
513 .await
514 .map_err(|_| Error::new("failed to send CloseBrowserSessionCommand"))?;
515
516 loop {
517 let event =
518 state.events.message().await?.ok_or_else(|| {
519 Error::new("browser session closed before close confirmation")
520 })?;
521
522 match event.event {
523 Some(BrowserEvent::Closed(_)) => {
524 state.closed = true;
525 return Ok(());
526 }
527 Some(BrowserEvent::Error(error)) => {
528 return Err(Error::new(format!(
529 "browser session error while closing: {}",
530 error.message
531 )));
532 }
533 _ => {}
534 }
535 }
536 }
537}
538
539impl Tab {
540 pub fn locator(&self, css_selector: impl Into<String>) -> Locator {
541 Locator {
542 page: self.clone(),
543 selector: css_selector.into(),
544 }
545 }
546
547 pub fn session_id(&self) -> &str {
548 &self.inner.session_id
549 }
550
551 pub async fn goto(&self, url: impl Into<String>) -> Result<NavigateResult> {
552 self.navigate(url).await
553 }
554
555 pub async fn ping(&self, message: impl Into<String>) -> Result<String> {
556 let mut state = self.inner.state.lock().await;
557 let handle = self.ensure_handle(&mut state).await?;
558 if handle.closed {
559 return Err(Error::new(format!(
560 "tab session {} is closed",
561 self.inner.session_id
562 )));
563 }
564
565 handle
566 .command_tx
567 .send(TabSessionCommand {
568 browser_session_id: self.inner.browser_session_id.clone(),
569 tab_session_id: self.inner.session_id.clone(),
570 command: Some(TabCommand::Ping(TabSessionPingCommand {
571 message: message.into(),
572 })),
573 })
574 .await
575 .map_err(|_| Error::new("failed to send TabSessionPingCommand"))?;
576
577 loop {
578 let event = handle
579 .events
580 .message()
581 .await?
582 .ok_or_else(|| Error::new("tab session closed while waiting for pong"))?;
583
584 match event.event {
585 Some(TabEvent::Attached(_)) => {}
586 Some(TabEvent::Pong(pong)) => return Ok(pong.message),
587 Some(TabEvent::Error(error)) => {
588 return Err(Error::new(format!(
589 "tab session error while pinging: {}",
590 error.message
591 )));
592 }
593 Some(TabEvent::Closed(_)) => {
594 handle.closed = true;
595 return Err(Error::new(format!(
596 "tab session {} closed while waiting for pong",
597 self.inner.session_id
598 )));
599 }
600 _ => {}
601 }
602 }
603 }
604
605 pub async fn navigate(&self, url: impl Into<String>) -> Result<NavigateResult> {
606 self.navigate_with_options(url, CommandOptions::default())
607 .await
608 }
609
610 pub async fn navigate_with_options(
611 &self,
612 url: impl Into<String>,
613 options: CommandOptions,
614 ) -> Result<NavigateResult> {
615 let mut state = self.inner.state.lock().await;
616 let handle = self.ensure_handle(&mut state).await?;
617 if handle.closed {
618 return Err(Error::new(format!(
619 "tab session {} is closed",
620 self.inner.session_id
621 )));
622 }
623
624 handle
625 .command_tx
626 .send(TabSessionCommand {
627 browser_session_id: self.inner.browser_session_id.clone(),
628 tab_session_id: self.inner.session_id.clone(),
629 command: Some(TabCommand::Navigate(NavigateTabCommand {
630 url: url.into(),
631 retry_options: command_retry_options(options.timeout_ms),
632 })),
633 })
634 .await
635 .map_err(|_| Error::new("failed to send NavigateTabCommand"))?;
636
637 let mut navigated = None;
638 let mut injection = None;
639
640 loop {
641 let event = handle
642 .events
643 .message()
644 .await?
645 .ok_or_else(|| Error::new("tab session closed while waiting for navigation"))?;
646
647 match event.event {
648 Some(TabEvent::Attached(_)) => {}
649 Some(TabEvent::Navigated(navigated_event)) => {
650 navigated = Some(navigated_event);
651 }
652 Some(TabEvent::ChromiumBidiInjection(injection_event)) => {
653 injection = Some(injection_event);
654 }
655 Some(TabEvent::Error(error)) => {
656 return Err(Error::new(format!(
657 "tab session error while navigating: {}",
658 error.message
659 )));
660 }
661 Some(TabEvent::Closed(_)) => {
662 handle.closed = true;
663 return Err(Error::new(format!(
664 "tab session {} closed while navigating",
665 self.inner.session_id
666 )));
667 }
668 _ => {}
669 }
670
671 if let (Some(navigated_event), Some(injection_event)) =
672 (navigated.take(), injection.take())
673 {
674 return Ok(NavigateResult {
675 url: navigated_event.url,
676 note: navigated_event.note,
677 bidi_session_id: injection_event.bidi_session_id,
678 mapper_target_id: injection_event.mapper_target_id,
679 mapper_session_id: injection_event.mapper_session_id,
680 package_version: injection_event.package_version,
681 });
682 }
683 }
684 }
685
686 pub async fn click(&self, css_selector: impl Into<String>) -> Result<ClickResult> {
687 self.click_with_options(css_selector, CommandOptions::default())
688 .await
689 }
690
691 pub async fn click_with_options(
692 &self,
693 css_selector: impl Into<String>,
694 options: CommandOptions,
695 ) -> Result<ClickResult> {
696 let mut state = self.inner.state.lock().await;
697 let handle = self.ensure_handle(&mut state).await?;
698 if handle.closed {
699 return Err(Error::new(format!(
700 "tab session {} is closed",
701 self.inner.session_id
702 )));
703 }
704
705 handle
706 .command_tx
707 .send(TabSessionCommand {
708 browser_session_id: self.inner.browser_session_id.clone(),
709 tab_session_id: self.inner.session_id.clone(),
710 command: Some(TabCommand::ClickElement(ClickElementCommand {
711 css_selector: css_selector.into(),
712 retry_options: command_retry_options(options.timeout_ms),
713 })),
714 })
715 .await
716 .map_err(|_| Error::new("failed to send ClickElementCommand"))?;
717
718 loop {
719 let event =
720 handle.events.message().await?.ok_or_else(|| {
721 Error::new("tab session closed while waiting for click result")
722 })?;
723
724 match event.event {
725 Some(TabEvent::Attached(_)) => {}
726 Some(TabEvent::ElementClicked(clicked)) => {
727 return Ok(ClickResult {
728 selector: clicked.css_selector,
729 note: clicked.note,
730 bidi_session_id: clicked.bidi_session_id,
731 });
732 }
733 Some(TabEvent::Error(error)) => {
734 return Err(Error::new(format!(
735 "tab session error while clicking: {}",
736 error.message
737 )));
738 }
739 Some(TabEvent::Closed(_)) => {
740 handle.closed = true;
741 return Err(Error::new(format!(
742 "tab session {} closed while waiting for click result",
743 self.inner.session_id
744 )));
745 }
746 _ => {}
747 }
748 }
749 }
750
751 pub async fn count(&self, css_selector: impl Into<String>) -> Result<CountResult> {
752 self.count_with_options(css_selector, CommandOptions::default())
753 .await
754 }
755
756 pub async fn count_with_options(
757 &self,
758 css_selector: impl Into<String>,
759 options: CommandOptions,
760 ) -> Result<CountResult> {
761 let mut state = self.inner.state.lock().await;
762 let handle = self.ensure_handle(&mut state).await?;
763 if handle.closed {
764 return Err(Error::new(format!(
765 "tab session {} is closed",
766 self.inner.session_id
767 )));
768 }
769
770 handle
771 .command_tx
772 .send(TabSessionCommand {
773 browser_session_id: self.inner.browser_session_id.clone(),
774 tab_session_id: self.inner.session_id.clone(),
775 command: Some(TabCommand::CountElements(CountElementsCommand {
776 css_selector: css_selector.into(),
777 retry_options: command_retry_options(options.timeout_ms),
778 })),
779 })
780 .await
781 .map_err(|_| Error::new("failed to send CountElementsCommand"))?;
782
783 loop {
784 let event =
785 handle.events.message().await?.ok_or_else(|| {
786 Error::new("tab session closed while waiting for count result")
787 })?;
788
789 match event.event {
790 Some(TabEvent::Attached(_)) => {}
791 Some(TabEvent::ElementCounted(counted)) => {
792 return Ok(count_result_from_event(counted));
793 }
794 Some(TabEvent::Error(error)) => {
795 return Err(Error::new(format!(
796 "tab session error while counting elements: {}",
797 error.message
798 )));
799 }
800 Some(TabEvent::Closed(_)) => {
801 handle.closed = true;
802 return Err(Error::new(format!(
803 "tab session {} closed while waiting for count result",
804 self.inner.session_id
805 )));
806 }
807 _ => {}
808 }
809 }
810 }
811
812 pub async fn highlight(&self, css_selector: impl Into<String>) -> Result<HighlightResult> {
813 self.highlight_with_options(css_selector, HighlightOptions::default())
814 .await
815 }
816
817 pub async fn highlight_with_options(
818 &self,
819 css_selector: impl Into<String>,
820 options: HighlightOptions,
821 ) -> Result<HighlightResult> {
822 let mut state = self.inner.state.lock().await;
823 let handle = self.ensure_handle(&mut state).await?;
824 if handle.closed {
825 return Err(Error::new(format!(
826 "tab session {} is closed",
827 self.inner.session_id
828 )));
829 }
830
831 handle
832 .command_tx
833 .send(TabSessionCommand {
834 browser_session_id: self.inner.browser_session_id.clone(),
835 tab_session_id: self.inner.session_id.clone(),
836 command: Some(TabCommand::HighlightElements(HighlightElementsCommand {
837 css_selector: css_selector.into(),
838 duration_ms: options.duration_ms,
839 retry_options: command_retry_options(options.timeout_ms),
840 })),
841 })
842 .await
843 .map_err(|_| Error::new("failed to send HighlightElementsCommand"))?;
844
845 loop {
846 let event = handle.events.message().await?.ok_or_else(|| {
847 Error::new("tab session closed while waiting for highlight result")
848 })?;
849
850 match event.event {
851 Some(TabEvent::Attached(_)) => {}
852 Some(TabEvent::ElementsHighlighted(highlighted)) => {
853 return Ok(highlight_result_from_event(highlighted));
854 }
855 Some(TabEvent::Error(error)) => {
856 return Err(Error::new(format!(
857 "tab session error while highlighting elements: {}",
858 error.message
859 )));
860 }
861 Some(TabEvent::Closed(_)) => {
862 handle.closed = true;
863 return Err(Error::new(format!(
864 "tab session {} closed while waiting for highlight result",
865 self.inner.session_id
866 )));
867 }
868 _ => {}
869 }
870 }
871 }
872
873 pub async fn focus(&self, css_selector: impl Into<String>) -> Result<ElementResult> {
874 self.focus_with_options(css_selector, CommandOptions::default())
875 .await
876 }
877
878 pub async fn focus_with_options(
879 &self,
880 css_selector: impl Into<String>,
881 options: CommandOptions,
882 ) -> Result<ElementResult> {
883 let mut state = self.inner.state.lock().await;
884 let handle = self.ensure_handle(&mut state).await?;
885 if handle.closed {
886 return Err(Error::new(format!(
887 "tab session {} is closed",
888 self.inner.session_id
889 )));
890 }
891 handle
892 .command_tx
893 .send(TabSessionCommand {
894 browser_session_id: self.inner.browser_session_id.clone(),
895 tab_session_id: self.inner.session_id.clone(),
896 command: Some(TabCommand::FocusElement(FocusElementCommand {
897 css_selector: css_selector.into(),
898 retry_options: command_retry_options(options.timeout_ms),
899 })),
900 })
901 .await
902 .map_err(|_| Error::new("failed to send FocusElementCommand"))?;
903 loop {
904 let event =
905 handle.events.message().await?.ok_or_else(|| {
906 Error::new("tab session closed while waiting for focus result")
907 })?;
908 match event.event {
909 Some(TabEvent::Attached(_)) => {}
910 Some(TabEvent::ElementFocused(focused)) => {
911 return Ok(ElementResult {
912 selector: focused.css_selector,
913 note: focused.note,
914 });
915 }
916 Some(TabEvent::Error(error)) => {
917 return Err(Error::new(format!(
918 "tab session error while focusing: {}",
919 error.message
920 )));
921 }
922 Some(TabEvent::Closed(_)) => {
923 handle.closed = true;
924 return Err(Error::new(format!(
925 "tab session {} closed while waiting for focus result",
926 self.inner.session_id
927 )));
928 }
929 _ => {}
930 }
931 }
932 }
933
934 pub async fn fill(
935 &self,
936 css_selector: impl Into<String>,
937 value: impl Into<String>,
938 ) -> Result<FillResult> {
939 self.fill_with_options(css_selector, value, CommandOptions::default())
940 .await
941 }
942
943 pub async fn fill_with_options(
944 &self,
945 css_selector: impl Into<String>,
946 value: impl Into<String>,
947 options: CommandOptions,
948 ) -> Result<FillResult> {
949 let mut state = self.inner.state.lock().await;
950 let handle = self.ensure_handle(&mut state).await?;
951 if handle.closed {
952 return Err(Error::new(format!(
953 "tab session {} is closed",
954 self.inner.session_id
955 )));
956 }
957 handle
958 .command_tx
959 .send(TabSessionCommand {
960 browser_session_id: self.inner.browser_session_id.clone(),
961 tab_session_id: self.inner.session_id.clone(),
962 command: Some(TabCommand::FillElement(FillElementCommand {
963 css_selector: css_selector.into(),
964 value: value.into(),
965 retry_options: command_retry_options(options.timeout_ms),
966 })),
967 })
968 .await
969 .map_err(|_| Error::new("failed to send FillElementCommand"))?;
970 loop {
971 let event =
972 handle.events.message().await?.ok_or_else(|| {
973 Error::new("tab session closed while waiting for fill result")
974 })?;
975 match event.event {
976 Some(TabEvent::Attached(_)) => {}
977 Some(TabEvent::ElementFilled(filled)) => {
978 return Ok(FillResult {
979 selector: filled.css_selector,
980 value: filled.value,
981 note: filled.note,
982 });
983 }
984 Some(TabEvent::Error(error)) => {
985 return Err(Error::new(format!(
986 "tab session error while filling: {}",
987 error.message
988 )));
989 }
990 Some(TabEvent::Closed(_)) => {
991 handle.closed = true;
992 return Err(Error::new(format!(
993 "tab session {} closed while waiting for fill result",
994 self.inner.session_id
995 )));
996 }
997 _ => {}
998 }
999 }
1000 }
1001
1002 pub async fn hover(&self, css_selector: impl Into<String>) -> Result<ElementResult> {
1003 self.hover_with_options(css_selector, CommandOptions::default())
1004 .await
1005 }
1006
1007 pub async fn hover_with_options(
1008 &self,
1009 css_selector: impl Into<String>,
1010 options: CommandOptions,
1011 ) -> Result<ElementResult> {
1012 let mut state = self.inner.state.lock().await;
1013 let handle = self.ensure_handle(&mut state).await?;
1014 if handle.closed {
1015 return Err(Error::new(format!(
1016 "tab session {} is closed",
1017 self.inner.session_id
1018 )));
1019 }
1020 handle
1021 .command_tx
1022 .send(TabSessionCommand {
1023 browser_session_id: self.inner.browser_session_id.clone(),
1024 tab_session_id: self.inner.session_id.clone(),
1025 command: Some(TabCommand::HoverElement(HoverElementCommand {
1026 css_selector: css_selector.into(),
1027 retry_options: command_retry_options(options.timeout_ms),
1028 })),
1029 })
1030 .await
1031 .map_err(|_| Error::new("failed to send HoverElementCommand"))?;
1032 loop {
1033 let event =
1034 handle.events.message().await?.ok_or_else(|| {
1035 Error::new("tab session closed while waiting for hover result")
1036 })?;
1037 match event.event {
1038 Some(TabEvent::Attached(_)) => {}
1039 Some(TabEvent::ElementHovered(hovered)) => {
1040 return Ok(ElementResult {
1041 selector: hovered.css_selector,
1042 note: hovered.note,
1043 });
1044 }
1045 Some(TabEvent::Error(error)) => {
1046 return Err(Error::new(format!(
1047 "tab session error while hovering: {}",
1048 error.message
1049 )));
1050 }
1051 Some(TabEvent::Closed(_)) => {
1052 handle.closed = true;
1053 return Err(Error::new(format!(
1054 "tab session {} closed while waiting for hover result",
1055 self.inner.session_id
1056 )));
1057 }
1058 _ => {}
1059 }
1060 }
1061 }
1062
1063 pub async fn press(
1064 &self,
1065 css_selector: impl Into<String>,
1066 key: impl Into<String>,
1067 ) -> Result<PressResult> {
1068 self.press_with_options(css_selector, key, PressOptions::default())
1069 .await
1070 }
1071
1072 pub async fn press_with_options(
1073 &self,
1074 css_selector: impl Into<String>,
1075 key: impl Into<String>,
1076 options: PressOptions,
1077 ) -> Result<PressResult> {
1078 let mut state = self.inner.state.lock().await;
1079 let handle = self.ensure_handle(&mut state).await?;
1080 if handle.closed {
1081 return Err(Error::new(format!(
1082 "tab session {} is closed",
1083 self.inner.session_id
1084 )));
1085 }
1086 handle
1087 .command_tx
1088 .send(TabSessionCommand {
1089 browser_session_id: self.inner.browser_session_id.clone(),
1090 tab_session_id: self.inner.session_id.clone(),
1091 command: Some(TabCommand::PressKey(PressKeyCommand {
1092 css_selector: css_selector.into(),
1093 key: key.into(),
1094 text: options.text,
1095 retry_options: command_retry_options(options.timeout_ms),
1096 })),
1097 })
1098 .await
1099 .map_err(|_| Error::new("failed to send PressKeyCommand"))?;
1100 loop {
1101 let event =
1102 handle.events.message().await?.ok_or_else(|| {
1103 Error::new("tab session closed while waiting for press result")
1104 })?;
1105 match event.event {
1106 Some(TabEvent::Attached(_)) => {}
1107 Some(TabEvent::KeyPressed(pressed)) => {
1108 return Ok(PressResult {
1109 selector: pressed.css_selector,
1110 key: pressed.key,
1111 note: pressed.note,
1112 });
1113 }
1114 Some(TabEvent::Error(error)) => {
1115 return Err(Error::new(format!(
1116 "tab session error while pressing key: {}",
1117 error.message
1118 )));
1119 }
1120 Some(TabEvent::Closed(_)) => {
1121 handle.closed = true;
1122 return Err(Error::new(format!(
1123 "tab session {} closed while waiting for press result",
1124 self.inner.session_id
1125 )));
1126 }
1127 _ => {}
1128 }
1129 }
1130 }
1131
1132 pub async fn text_content(&self, css_selector: impl Into<String>) -> Result<TextResult> {
1133 self.text_content_with_options(css_selector, CommandOptions::default())
1134 .await
1135 }
1136
1137 pub async fn text_content_with_options(
1138 &self,
1139 css_selector: impl Into<String>,
1140 options: CommandOptions,
1141 ) -> Result<TextResult> {
1142 self.read_text(css_selector.into(), options, true).await
1143 }
1144
1145 pub async fn inner_text(&self, css_selector: impl Into<String>) -> Result<TextResult> {
1146 self.inner_text_with_options(css_selector, CommandOptions::default())
1147 .await
1148 }
1149
1150 pub async fn inner_text_with_options(
1151 &self,
1152 css_selector: impl Into<String>,
1153 options: CommandOptions,
1154 ) -> Result<TextResult> {
1155 self.read_text(css_selector.into(), options, false).await
1156 }
1157
1158 pub async fn wait_for_selector(
1159 &self,
1160 css_selector: impl Into<String>,
1161 ) -> Result<WaitForSelectorResult> {
1162 self.wait_for_selector_with_options(css_selector, WaitForSelectorOptions::default())
1163 .await
1164 }
1165
1166 pub async fn wait_for_selector_with_options(
1167 &self,
1168 css_selector: impl Into<String>,
1169 options: WaitForSelectorOptions,
1170 ) -> Result<WaitForSelectorResult> {
1171 let mut state = self.inner.state.lock().await;
1172 let handle = self.ensure_handle(&mut state).await?;
1173 if handle.closed {
1174 return Err(Error::new(format!(
1175 "tab session {} is closed",
1176 self.inner.session_id
1177 )));
1178 }
1179 handle
1180 .command_tx
1181 .send(TabSessionCommand {
1182 browser_session_id: self.inner.browser_session_id.clone(),
1183 tab_session_id: self.inner.session_id.clone(),
1184 command: Some(TabCommand::WaitForSelector(WaitForSelectorCommand {
1185 css_selector: css_selector.into(),
1186 visible: options.visible,
1187 retry_options: command_retry_options(options.timeout_ms),
1188 })),
1189 })
1190 .await
1191 .map_err(|_| Error::new("failed to send WaitForSelectorCommand"))?;
1192 loop {
1193 let event = handle
1194 .events
1195 .message()
1196 .await?
1197 .ok_or_else(|| Error::new("tab session closed while waiting for selector"))?;
1198 match event.event {
1199 Some(TabEvent::Attached(_)) => {}
1200 Some(TabEvent::SelectorWaitSatisfied(waited)) => {
1201 return Ok(WaitForSelectorResult {
1202 selector: waited.css_selector,
1203 visible: waited.visible,
1204 note: waited.note,
1205 });
1206 }
1207 Some(TabEvent::Error(error)) => {
1208 return Err(Error::new(format!(
1209 "tab session error while waiting for selector: {}",
1210 error.message
1211 )));
1212 }
1213 Some(TabEvent::Closed(_)) => {
1214 handle.closed = true;
1215 return Err(Error::new(format!(
1216 "tab session {} closed while waiting for selector result",
1217 self.inner.session_id
1218 )));
1219 }
1220 _ => {}
1221 }
1222 }
1223 }
1224
1225 async fn read_text(
1226 &self,
1227 css_selector: String,
1228 options: CommandOptions,
1229 text_content: bool,
1230 ) -> Result<TextResult> {
1231 let mut state = self.inner.state.lock().await;
1232 let handle = self.ensure_handle(&mut state).await?;
1233 if handle.closed {
1234 return Err(Error::new(format!(
1235 "tab session {} is closed",
1236 self.inner.session_id
1237 )));
1238 }
1239 let command = if text_content {
1240 TabCommand::GetTextContent(GetTextContentCommand {
1241 css_selector,
1242 retry_options: command_retry_options(options.timeout_ms),
1243 })
1244 } else {
1245 TabCommand::GetInnerText(GetInnerTextCommand {
1246 css_selector,
1247 retry_options: command_retry_options(options.timeout_ms),
1248 })
1249 };
1250 handle
1251 .command_tx
1252 .send(TabSessionCommand {
1253 browser_session_id: self.inner.browser_session_id.clone(),
1254 tab_session_id: self.inner.session_id.clone(),
1255 command: Some(command),
1256 })
1257 .await
1258 .map_err(|_| Error::new("failed to send text command"))?;
1259 loop {
1260 let event =
1261 handle.events.message().await?.ok_or_else(|| {
1262 Error::new("tab session closed while waiting for text result")
1263 })?;
1264 match event.event {
1265 Some(TabEvent::Attached(_)) => {}
1266 Some(TabEvent::TextContentResolved(text)) => {
1267 return Ok(TextResult {
1268 selector: text.css_selector,
1269 text: text.text,
1270 note: text.note,
1271 });
1272 }
1273 Some(TabEvent::InnerTextResolved(text)) => {
1274 return Ok(TextResult {
1275 selector: text.css_selector,
1276 text: text.text,
1277 note: text.note,
1278 });
1279 }
1280 Some(TabEvent::Error(error)) => {
1281 return Err(Error::new(format!(
1282 "tab session error while reading text: {}",
1283 error.message
1284 )));
1285 }
1286 Some(TabEvent::Closed(_)) => {
1287 handle.closed = true;
1288 return Err(Error::new(format!(
1289 "tab session {} closed while waiting for text result",
1290 self.inner.session_id
1291 )));
1292 }
1293 _ => {}
1294 }
1295 }
1296 }
1297
1298 pub async fn close(&self) -> Result<()> {
1299 let mut state = self.inner.state.lock().await;
1300 let handle = self.ensure_handle(&mut state).await?;
1301 if handle.closed {
1302 return Ok(());
1303 }
1304
1305 handle
1306 .command_tx
1307 .send(TabSessionCommand {
1308 browser_session_id: self.inner.browser_session_id.clone(),
1309 tab_session_id: self.inner.session_id.clone(),
1310 command: Some(TabCommand::Close(CloseTabSessionCommand {})),
1311 })
1312 .await
1313 .map_err(|_| Error::new("failed to send CloseTabSessionCommand"))?;
1314
1315 loop {
1316 let event = handle
1317 .events
1318 .message()
1319 .await?
1320 .ok_or_else(|| Error::new("tab session closed before close confirmation"))?;
1321
1322 match event.event {
1323 Some(TabEvent::Attached(_)) => {}
1324 Some(TabEvent::Closed(_)) => {
1325 handle.closed = true;
1326 return Ok(());
1327 }
1328 Some(TabEvent::Error(error)) => {
1329 return Err(Error::new(format!(
1330 "tab session error while closing: {}",
1331 error.message
1332 )));
1333 }
1334 _ => {}
1335 }
1336 }
1337 }
1338
1339 async fn ensure_handle<'a>(&self, state: &'a mut TabState) -> Result<&'a mut TabHandle> {
1340 if state.handle.is_none() {
1341 let mut engine = self.inner.runtime.engine.clone();
1342 let (command_tx, command_rx) = mpsc::channel(16);
1343 let response = engine
1344 .tab_session(tonic::Request::new(ReceiverStream::new(command_rx)))
1345 .await?;
1346 state.handle = Some(TabHandle {
1347 command_tx,
1348 events: response.into_inner(),
1349 closed: false,
1350 });
1351 }
1352
1353 state
1354 .handle
1355 .as_mut()
1356 .ok_or_else(|| Error::new("tab session handle was not initialized"))
1357 }
1358}
1359
1360impl BrowserType {
1361 pub async fn launch(&self, options: LaunchOptions) -> Result<Browser> {
1362 launch_browser(self.browser_kind, options).await
1363 }
1364}
1365
1366impl Locator {
1367 pub fn page(&self) -> &Page {
1368 &self.page
1369 }
1370
1371 pub fn selector(&self) -> &str {
1372 &self.selector
1373 }
1374
1375 pub fn locator(&self, css_selector: impl Into<String>) -> Locator {
1376 Locator {
1377 page: self.page.clone(),
1378 selector: format!("{} {}", self.selector, css_selector.into()),
1379 }
1380 }
1381
1382 pub async fn click(&self) -> Result<ClickResult> {
1383 self.page.click(self.selector.clone()).await
1384 }
1385
1386 pub async fn count(&self) -> Result<CountResult> {
1387 self.page.count(self.selector.clone()).await
1388 }
1389
1390 pub async fn highlight(&self) -> Result<HighlightResult> {
1391 self.page.highlight(self.selector.clone()).await
1392 }
1393
1394 pub async fn focus(&self) -> Result<ElementResult> {
1395 self.page.focus(self.selector.clone()).await
1396 }
1397
1398 pub async fn fill(&self, value: impl Into<String>) -> Result<FillResult> {
1399 self.page.fill(self.selector.clone(), value.into()).await
1400 }
1401
1402 pub async fn hover(&self) -> Result<ElementResult> {
1403 self.page.hover(self.selector.clone()).await
1404 }
1405
1406 pub async fn press(&self, key: impl Into<String>) -> Result<PressResult> {
1407 self.page.press(self.selector.clone(), key.into()).await
1408 }
1409
1410 pub async fn text_content(&self) -> Result<TextResult> {
1411 self.page.text_content(self.selector.clone()).await
1412 }
1413
1414 pub async fn inner_text(&self) -> Result<TextResult> {
1415 self.page.inner_text(self.selector.clone()).await
1416 }
1417
1418 pub async fn wait_for(&self) -> Result<WaitForSelectorResult> {
1419 self.page.wait_for_selector(self.selector.clone()).await
1420 }
1421}
1422
1423async fn get_runtime() -> Result<Arc<RuntimeClient>> {
1424 if let Ok(runtime) = runtime_slot().lock() {
1425 if let Some(existing) = runtime.as_ref() {
1426 return Ok(Arc::clone(existing));
1427 }
1428 }
1429
1430 let endpoint = configured_server_addr();
1431 let engine = EngineServiceClient::connect(endpoint).await?;
1432 let runtime = Arc::new(RuntimeClient { engine });
1433
1434 let mut slot = runtime_slot()
1435 .lock()
1436 .map_err(|_| Error::new("runtime singleton lock is poisoned"))?;
1437 if let Some(existing) = slot.as_ref() {
1438 return Ok(Arc::clone(existing));
1439 }
1440 *slot = Some(Arc::clone(&runtime));
1441 Ok(runtime)
1442}
1443
1444fn runtime_slot() -> &'static Mutex<Option<Arc<RuntimeClient>>> {
1445 RUNTIME.get_or_init(|| Mutex::new(None))
1446}
1447
1448fn server_addr_override_slot() -> &'static Mutex<Option<String>> {
1449 SERVER_ADDR_OVERRIDE.get_or_init(|| Mutex::new(None))
1450}
1451
1452fn configured_server_addr() -> String {
1453 if let Ok(server_addr_override) = server_addr_override_slot().lock() {
1454 if let Some(server_addr) = server_addr_override.as_ref() {
1455 return server_addr.clone();
1456 }
1457 }
1458
1459 normalize_server_addr(
1460 std::env::var(SERVER_ADDR_ENV_VAR)
1461 .ok()
1462 .filter(|value| !value.trim().is_empty())
1463 .as_deref()
1464 .unwrap_or(DEFAULT_SERVER_ADDR),
1465 )
1466}
1467
1468fn normalize_server_addr(raw: &str) -> String {
1469 let trimmed = raw.trim();
1470 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
1471 trimmed.to_string()
1472 } else {
1473 format!("http://{trimmed}")
1474 }
1475}
1476
1477fn command_retry_options(timeout_ms: Option<u32>) -> Option<CommandRetryOptions> {
1478 timeout_ms.map(|timeout_ms| CommandRetryOptions {
1479 timeout_ms: Some(timeout_ms),
1480 retry_interval_ms: None,
1481 })
1482}
1483
1484fn count_result_from_event(event: ElementCountedEvent) -> CountResult {
1485 CountResult {
1486 selector: event.css_selector,
1487 count: event.count,
1488 note: event.note,
1489 }
1490}
1491
1492fn highlight_result_from_event(event: ElementsHighlightedEvent) -> HighlightResult {
1493 HighlightResult {
1494 selector: event.css_selector,
1495 count: event.count,
1496 note: event.note,
1497 }
1498}