Skip to main content

allwright/
client.rs

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