Skip to main content

ios_core/services/webinspector/
mod.rs

1use std::collections::VecDeque;
2
3use indexmap::IndexMap;
4use serde::Serialize;
5use serde_json::json;
6use serde_json::Value as JsonValue;
7use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
8use tokio::time::{timeout, Duration, Instant};
9use uuid::Uuid;
10
11pub const SERVICE_NAME: &str = "com.apple.webinspector";
12pub const RSD_SERVICE_NAME: &str = "com.apple.webinspector.shim.remote";
13pub const SAFARI_BUNDLE_ID: &str = "com.apple.mobilesafari";
14const MAX_PLIST_SIZE: usize = 16 * 1024 * 1024;
15
16service_error!(
17    WebInspectorError,
18    between {
19    #[error("JSON error: {0}")]
20    Json(#[from] serde_json::Error),
21    },
22    after {
23    #[error("timed out waiting for webinspector response after {0:?}")]
24    Timeout(Duration),
25    },
26);
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub enum AutomationAvailability {
31    NotAvailable,
32    Available,
33    Unknown(String),
34}
35
36impl AutomationAvailability {
37    fn from_wire(value: &str) -> Self {
38        match value {
39            "WIRAutomationAvailabilityNotAvailable" => Self::NotAvailable,
40            "WIRAutomationAvailabilityAvailable" => Self::Available,
41            other => Self::Unknown(other.to_string()),
42        }
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "snake_case")]
48pub enum WirType {
49    Automation,
50    Itml,
51    JavaScript,
52    Page,
53    ServiceWorker,
54    Web,
55    WebPage,
56    AutomaticallyPause,
57    Unknown(String),
58}
59
60impl WirType {
61    fn from_wire(value: &str) -> Self {
62        match value {
63            "WIRTypeAutomation" => Self::Automation,
64            "WIRTypeITML" => Self::Itml,
65            "WIRTypeJavaScript" => Self::JavaScript,
66            "WIRTypePage" => Self::Page,
67            "WIRTypeServiceWorker" => Self::ServiceWorker,
68            "WIRTypeWeb" => Self::Web,
69            "WIRTypeWebPage" => Self::WebPage,
70            "WIRAutomaticallyPause" => Self::AutomaticallyPause,
71            other => Self::Unknown(other.to_string()),
72        }
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77pub struct Application {
78    pub id: String,
79    pub bundle_identifier: String,
80    pub pid: u64,
81    pub name: String,
82    pub availability: AutomationAvailability,
83    pub is_active: bool,
84    pub is_proxy: bool,
85    pub is_ready: bool,
86    pub host_application_identifier: Option<String>,
87}
88
89impl Application {
90    fn from_plist(dict: &plist::Dictionary) -> Result<Self, WebInspectorError> {
91        let id = required_string(dict, "WIRApplicationIdentifierKey")?.to_string();
92        Ok(Self {
93            pid: pid_from_identifier(&id)?,
94            id,
95            bundle_identifier: required_string(dict, "WIRApplicationBundleIdentifierKey")?
96                .to_string(),
97            name: required_string(dict, "WIRApplicationNameKey")?.to_string(),
98            availability: AutomationAvailability::from_wire(required_string(
99                dict,
100                "WIRAutomationAvailabilityKey",
101            )?),
102            is_active: required_bool(dict, "WIRIsApplicationActiveKey")?,
103            is_proxy: required_bool(dict, "WIRIsApplicationProxyKey")?,
104            is_ready: required_bool(dict, "WIRIsApplicationReadyKey")?,
105            host_application_identifier: optional_string(dict, "WIRHostApplicationIdentifierKey")
106                .map(ToOwned::to_owned),
107        })
108    }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
112pub struct Page {
113    pub id: u64,
114    pub listing_key: String,
115    pub page_type: WirType,
116    pub title: Option<String>,
117    pub url: Option<String>,
118    pub automation_is_paired: Option<bool>,
119    pub automation_name: Option<String>,
120    pub automation_version: Option<String>,
121    pub automation_session_id: Option<String>,
122    pub automation_connection_id: Option<String>,
123}
124
125impl Page {
126    fn from_plist(listing_key: &str, dict: &plist::Dictionary) -> Result<Self, WebInspectorError> {
127        let id = match dict.get("WIRPageIdentifierKey") {
128            Some(value) => plist_integer_to_u64(value).ok_or_else(|| {
129                WebInspectorError::Protocol("WIRPageIdentifierKey must be an integer".to_string())
130            })?,
131            None => listing_key.parse::<u64>().map_err(|_| {
132                WebInspectorError::Protocol(format!(
133                    "missing WIRPageIdentifierKey and listing key '{listing_key}' is not numeric"
134                ))
135            })?,
136        };
137
138        Ok(Self {
139            id,
140            listing_key: listing_key.to_string(),
141            page_type: WirType::from_wire(required_string(dict, "WIRTypeKey")?),
142            title: optional_string(dict, "WIRTitleKey").map(ToOwned::to_owned),
143            url: optional_string(dict, "WIRURLKey").map(ToOwned::to_owned),
144            automation_is_paired: optional_bool(dict, "WIRAutomationTargetIsPairedKey"),
145            automation_name: optional_string(dict, "WIRAutomationTargetNameKey")
146                .map(ToOwned::to_owned),
147            automation_version: optional_string(dict, "WIRAutomationTargetVersionKey")
148                .map(ToOwned::to_owned),
149            automation_session_id: optional_string(dict, "WIRSessionIdentifierKey")
150                .map(ToOwned::to_owned),
151            automation_connection_id: optional_string(dict, "WIRConnectionIdentifierKey")
152                .map(ToOwned::to_owned),
153        })
154    }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct ApplicationPage {
159    pub application: Application,
160    pub page: Page,
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize)]
164#[serde(tag = "kind", rename_all = "snake_case")]
165pub enum WebInspectorEvent {
166    CurrentState {
167        availability: AutomationAvailability,
168    },
169    ConnectedApplications {
170        applications: Vec<Application>,
171    },
172    ConnectedDrivers,
173    Listing {
174        application_id: String,
175        pages: Vec<Page>,
176    },
177    ApplicationUpdated {
178        application: Application,
179    },
180    ApplicationConnected {
181        application: Application,
182    },
183    SocketData {
184        application_id: Option<String>,
185        message: JsonValue,
186    },
187    ApplicationDisconnected {
188        application_id: String,
189    },
190}
191
192#[derive(Debug)]
193pub struct WebInspectorClient<S> {
194    stream: S,
195    connection_id: String,
196    automation_availability: Option<AutomationAvailability>,
197    applications: IndexMap<String, Application>,
198    application_pages: IndexMap<String, IndexMap<u64, Page>>,
199    pending_events: VecDeque<WebInspectorEvent>,
200}
201
202impl<S: AsyncRead + AsyncWrite + Unpin> WebInspectorClient<S> {
203    pub fn new(stream: S) -> Self {
204        Self::with_connection_id(stream, Uuid::new_v4().to_string().to_uppercase())
205    }
206
207    pub fn with_connection_id(stream: S, connection_id: impl Into<String>) -> Self {
208        Self {
209            stream,
210            connection_id: connection_id.into(),
211            automation_availability: None,
212            applications: IndexMap::new(),
213            application_pages: IndexMap::new(),
214            pending_events: VecDeque::new(),
215        }
216    }
217
218    pub fn connection_id(&self) -> &str {
219        &self.connection_id
220    }
221
222    pub fn automation_availability(&self) -> Option<AutomationAvailability> {
223        self.automation_availability.clone()
224    }
225
226    pub fn applications(&self) -> &IndexMap<String, Application> {
227        &self.applications
228    }
229
230    pub fn application_pages(&self, application_id: &str) -> Option<&IndexMap<u64, Page>> {
231        self.application_pages.get(application_id)
232    }
233
234    pub fn application_by_bundle(&self, bundle_identifier: &str) -> Option<&Application> {
235        self.applications
236            .values()
237            .find(|application| application.bundle_identifier == bundle_identifier)
238    }
239
240    pub fn page(&self, application_id: &str, page_id: u64) -> Option<&Page> {
241        self.application_pages
242            .get(application_id)
243            .and_then(|pages| pages.get(&page_id))
244    }
245
246    pub fn automation_page_by_session(
247        &self,
248        application_id: &str,
249        session_id: &str,
250    ) -> Option<&Page> {
251        self.application_pages
252            .get(application_id)
253            .and_then(|pages| {
254                pages.values().find(|page| {
255                    page.page_type == WirType::Automation
256                        && page.automation_session_id.as_deref() == Some(session_id)
257                })
258            })
259    }
260
261    pub fn open_pages_snapshot(&self) -> Vec<ApplicationPage> {
262        let mut result = Vec::new();
263        for (application_id, application) in &self.applications {
264            if let Some(pages) = self.application_pages.get(application_id) {
265                for page in pages.values() {
266                    result.push(ApplicationPage {
267                        application: application.clone(),
268                        page: page.clone(),
269                    });
270                }
271            }
272        }
273        result
274    }
275
276    pub async fn start(&mut self, timeout_duration: Duration) -> Result<(), WebInspectorError> {
277        self.report_identifier().await?;
278        let deadline = Instant::now() + timeout_duration;
279        loop {
280            let event = self
281                .next_event_with_timeout(remaining_time(deadline, timeout_duration)?)
282                .await?;
283            if matches!(event, WebInspectorEvent::CurrentState { .. }) {
284                return Ok(());
285            }
286        }
287    }
288
289    /// Discovers all open application pages on the device.
290    ///
291    /// Sends `_rpc_getConnectedApplications:` and consumes incoming events until no
292    /// new event arrives within `idle_timeout`. The timeout signals quiescence (no
293    /// more pages are being reported) and is the **expected completion path** — it is
294    /// not an error. Returns the accumulated snapshot of open pages.
295    pub async fn open_application_pages(
296        &mut self,
297        idle_timeout: Duration,
298    ) -> Result<Vec<ApplicationPage>, WebInspectorError> {
299        self.request_connected_applications().await?;
300        loop {
301            match self.next_event_with_timeout(idle_timeout).await {
302                Ok(_) => continue,
303                Err(WebInspectorError::Timeout(_)) => return Ok(self.open_pages_snapshot()),
304                Err(error) => return Err(error),
305            }
306        }
307    }
308
309    pub async fn report_identifier(&mut self) -> Result<(), WebInspectorError> {
310        self.send_message("_rpc_reportIdentifier:", plist::Dictionary::new())
311            .await
312    }
313
314    pub async fn request_connected_applications(&mut self) -> Result<(), WebInspectorError> {
315        self.send_message("_rpc_getConnectedApplications:", plist::Dictionary::new())
316            .await
317    }
318
319    pub async fn request_listing(&mut self, application_id: &str) -> Result<(), WebInspectorError> {
320        self.send_message(
321            "_rpc_forwardGetListing:",
322            plist::Dictionary::from_iter([(
323                "WIRApplicationIdentifierKey".to_string(),
324                plist::Value::String(application_id.to_string()),
325            )]),
326        )
327        .await
328    }
329
330    pub async fn request_application_launch(
331        &mut self,
332        bundle_identifier: &str,
333    ) -> Result<(), WebInspectorError> {
334        self.send_message(
335            "_rpc_requestApplicationLaunch:",
336            plist::Dictionary::from_iter([(
337                "WIRApplicationBundleIdentifierKey".to_string(),
338                plist::Value::String(bundle_identifier.to_string()),
339            )]),
340        )
341        .await
342    }
343
344    pub async fn request_automation_session(
345        &mut self,
346        session_id: &str,
347        application_id: &str,
348    ) -> Result<(), WebInspectorError> {
349        self.send_message(
350            "_rpc_forwardAutomationSessionRequest:",
351            plist::Dictionary::from_iter([
352                (
353                    "WIRApplicationIdentifierKey".to_string(),
354                    plist::Value::String(application_id.to_string()),
355                ),
356                (
357                    "WIRSessionCapabilitiesKey".to_string(),
358                    plist::Value::Dictionary(plist::Dictionary::from_iter([
359                        (
360                            "org.webkit.webdriver.webrtc.allow-insecure-media-capture".to_string(),
361                            plist::Value::Boolean(true),
362                        ),
363                        (
364                            "org.webkit.webdriver.webrtc.suppress-ice-candidate-filtering"
365                                .to_string(),
366                            plist::Value::Boolean(false),
367                        ),
368                    ])),
369                ),
370                (
371                    "WIRSessionIdentifierKey".to_string(),
372                    plist::Value::String(session_id.to_string()),
373                ),
374            ]),
375        )
376        .await
377    }
378
379    pub async fn send_socket_setup(
380        &mut self,
381        session_id: &str,
382        application_id: &str,
383        page_id: u64,
384        pause: bool,
385    ) -> Result<(), WebInspectorError> {
386        let mut args = plist::Dictionary::from_iter([
387            (
388                "WIRApplicationIdentifierKey".to_string(),
389                plist::Value::String(application_id.to_string()),
390            ),
391            (
392                "WIRPageIdentifierKey".to_string(),
393                plist::Value::Integer(page_id.into()),
394            ),
395            (
396                "WIRSenderKey".to_string(),
397                plist::Value::String(session_id.to_string()),
398            ),
399            (
400                "WIRMessageDataTypeChunkSupportedKey".to_string(),
401                plist::Value::Integer(0.into()),
402            ),
403        ]);
404        if !pause {
405            args.insert(
406                "WIRAutomaticallyPause".to_string(),
407                plist::Value::Boolean(false),
408            );
409        }
410        self.send_message("_rpc_forwardSocketSetup:", args).await
411    }
412
413    pub async fn send_socket_data(
414        &mut self,
415        session_id: &str,
416        application_id: &str,
417        page_id: u64,
418        message: &JsonValue,
419    ) -> Result<(), WebInspectorError> {
420        self.send_message(
421            "_rpc_forwardSocketData:",
422            plist::Dictionary::from_iter([
423                (
424                    "WIRApplicationIdentifierKey".to_string(),
425                    plist::Value::String(application_id.to_string()),
426                ),
427                (
428                    "WIRPageIdentifierKey".to_string(),
429                    plist::Value::Integer(page_id.into()),
430                ),
431                (
432                    "WIRSessionIdentifierKey".to_string(),
433                    plist::Value::String(session_id.to_string()),
434                ),
435                (
436                    "WIRSenderKey".to_string(),
437                    plist::Value::String(session_id.to_string()),
438                ),
439                (
440                    "WIRSocketDataKey".to_string(),
441                    plist::Value::Data(serde_json::to_vec(message)?),
442                ),
443            ]),
444        )
445        .await
446    }
447
448    pub async fn next_event(&mut self) -> Result<WebInspectorEvent, WebInspectorError> {
449        if let Some(event) = self.pending_events.pop_front() {
450            return Ok(event);
451        }
452        let plist = recv_plist(&mut self.stream).await?;
453        self.handle_message(plist).await
454    }
455
456    pub async fn next_event_with_timeout(
457        &mut self,
458        timeout_duration: Duration,
459    ) -> Result<WebInspectorEvent, WebInspectorError> {
460        timeout(timeout_duration, self.next_event())
461            .await
462            .map_err(|_| WebInspectorError::Timeout(timeout_duration))?
463    }
464
465    async fn next_socket_data_with_timeout(
466        &mut self,
467        timeout_duration: Duration,
468    ) -> Result<WebInspectorEvent, WebInspectorError> {
469        let deadline = Instant::now() + timeout_duration;
470        loop {
471            let event = self
472                .next_event_with_timeout(remaining_time(deadline, timeout_duration)?)
473                .await?;
474            if matches!(event, WebInspectorEvent::SocketData { .. }) {
475                return Ok(event);
476            }
477        }
478    }
479
480    fn restore_pending_events_front(&mut self, mut events: Vec<WebInspectorEvent>) {
481        while let Some(event) = events.pop() {
482            self.pending_events.push_front(event);
483        }
484    }
485
486    async fn handle_message(
487        &mut self,
488        message: plist::Dictionary,
489    ) -> Result<WebInspectorEvent, WebInspectorError> {
490        let selector = required_string(&message, "__selector")?;
491        let argument = message
492            .get("__argument")
493            .and_then(plist::Value::as_dictionary)
494            .ok_or_else(|| {
495                WebInspectorError::Protocol(format!(
496                    "webinspector message '{selector}' missing __argument dictionary"
497                ))
498            })?;
499
500        match selector {
501            "_rpc_reportCurrentState:" => {
502                let availability = AutomationAvailability::from_wire(required_string(
503                    argument,
504                    "WIRAutomationAvailabilityKey",
505                )?);
506                self.automation_availability = Some(availability.clone());
507                Ok(WebInspectorEvent::CurrentState { availability })
508            }
509            "_rpc_reportConnectedApplicationList:" => {
510                let applications_dict = argument
511                    .get("WIRApplicationDictionaryKey")
512                    .and_then(plist::Value::as_dictionary)
513                    .ok_or_else(|| {
514                        WebInspectorError::Protocol(
515                            "connected application list missing WIRApplicationDictionaryKey"
516                                .to_string(),
517                        )
518                    })?;
519                let mut applications = IndexMap::new();
520                for application in applications_dict.values() {
521                    let application = application.as_dictionary().ok_or_else(|| {
522                        WebInspectorError::Protocol(
523                            "connected application entry was not a dictionary".to_string(),
524                        )
525                    })?;
526                    let application = Application::from_plist(application)?;
527                    applications.insert(application.id.clone(), application);
528                }
529
530                self.application_pages
531                    .retain(|application_id, _| applications.contains_key(application_id));
532                self.applications = applications.clone();
533                for application_id in applications.keys() {
534                    self.request_listing(application_id).await?;
535                }
536
537                Ok(WebInspectorEvent::ConnectedApplications {
538                    applications: applications.into_values().collect(),
539                })
540            }
541            "_rpc_reportConnectedDriverList:" => Ok(WebInspectorEvent::ConnectedDrivers),
542            "_rpc_applicationSentListing:" => {
543                let application_id =
544                    required_string(argument, "WIRApplicationIdentifierKey")?.to_string();
545                let listing = argument
546                    .get("WIRListingKey")
547                    .and_then(plist::Value::as_dictionary)
548                    .ok_or_else(|| {
549                        WebInspectorError::Protocol(
550                            "application listing missing WIRListingKey dictionary".to_string(),
551                        )
552                    })?;
553
554                let pages = self
555                    .application_pages
556                    .entry(application_id.clone())
557                    .or_default();
558                let mut listed_pages = Vec::with_capacity(listing.len());
559                for (listing_key, page) in listing {
560                    let page = page.as_dictionary().ok_or_else(|| {
561                        WebInspectorError::Protocol(
562                            "application page entry was not a dictionary".to_string(),
563                        )
564                    })?;
565                    let page = Page::from_plist(listing_key, page)?;
566                    pages.insert(page.id, page.clone());
567                    listed_pages.push(page);
568                }
569
570                Ok(WebInspectorEvent::Listing {
571                    application_id,
572                    pages: listed_pages,
573                })
574            }
575            "_rpc_applicationUpdated:" => {
576                let application = Application::from_plist(argument)?;
577                self.applications
578                    .insert(application.id.clone(), application.clone());
579                Ok(WebInspectorEvent::ApplicationUpdated { application })
580            }
581            "_rpc_applicationConnected:" => {
582                let application = Application::from_plist(argument)?;
583                self.applications
584                    .insert(application.id.clone(), application.clone());
585                Ok(WebInspectorEvent::ApplicationConnected { application })
586            }
587            "_rpc_applicationSentData:" => {
588                let payload = extract_json_payload(argument, "WIRMessageDataKey")?;
589                let application_id =
590                    optional_string(argument, "WIRApplicationIdentifierKey").map(ToOwned::to_owned);
591                Ok(WebInspectorEvent::SocketData {
592                    application_id,
593                    message: payload,
594                })
595            }
596            "_rpc_applicationDisconnected:" => {
597                let application_id =
598                    required_string(argument, "WIRApplicationIdentifierKey")?.to_string();
599                self.applications.shift_remove(&application_id);
600                self.application_pages.shift_remove(&application_id);
601                Ok(WebInspectorEvent::ApplicationDisconnected { application_id })
602            }
603            other => Err(WebInspectorError::Protocol(format!(
604                "unsupported webinspector selector '{other}'"
605            ))),
606        }
607    }
608
609    async fn send_message(
610        &mut self,
611        selector: &str,
612        mut arguments: plist::Dictionary,
613    ) -> Result<(), WebInspectorError> {
614        arguments.insert(
615            "WIRConnectionIdentifierKey".to_string(),
616            plist::Value::String(self.connection_id.clone()),
617        );
618        send_plist(
619            &mut self.stream,
620            &plist::Value::Dictionary(plist::Dictionary::from_iter([
621                (
622                    "__selector".to_string(),
623                    plist::Value::String(selector.to_string()),
624                ),
625                (
626                    "__argument".to_string(),
627                    plist::Value::Dictionary(arguments),
628                ),
629            ])),
630        )
631        .await
632    }
633}
634
635#[derive(Debug, Clone)]
636pub struct InspectorSession {
637    application_id: String,
638    page_id: u64,
639    session_id: String,
640    target_id: Option<String>,
641    next_transport_id: u64,
642    next_command_id: u64,
643}
644
645impl InspectorSession {
646    pub fn new(application_id: impl Into<String>, page_id: u64) -> Self {
647        Self::with_session_id(
648            application_id,
649            page_id,
650            Uuid::new_v4().to_string().to_uppercase(),
651        )
652    }
653
654    pub fn with_session_id(
655        application_id: impl Into<String>,
656        page_id: u64,
657        session_id: impl Into<String>,
658    ) -> Self {
659        Self {
660            application_id: application_id.into(),
661            page_id,
662            session_id: session_id.into(),
663            target_id: None,
664            next_transport_id: 1,
665            next_command_id: 1,
666        }
667    }
668
669    pub fn session_id(&self) -> &str {
670        &self.session_id
671    }
672
673    pub fn application_id(&self) -> &str {
674        &self.application_id
675    }
676
677    pub fn page_id(&self) -> u64 {
678        self.page_id
679    }
680
681    pub fn target_id(&self) -> Option<&str> {
682        self.target_id.as_deref()
683    }
684
685    pub async fn attach<S: AsyncRead + AsyncWrite + Unpin>(
686        &mut self,
687        client: &mut WebInspectorClient<S>,
688        wait_for_target: bool,
689        timeout_duration: Duration,
690    ) -> Result<(), WebInspectorError> {
691        client
692            .send_socket_setup(&self.session_id, &self.application_id, self.page_id, true)
693            .await?;
694        if wait_for_target {
695            self.wait_for_target(client, timeout_duration).await?;
696        }
697        Ok(())
698    }
699
700    pub async fn next_raw_message<S: AsyncRead + AsyncWrite + Unpin>(
701        &mut self,
702        client: &mut WebInspectorClient<S>,
703        timeout_duration: Duration,
704    ) -> Result<JsonValue, WebInspectorError> {
705        let event = client
706            .next_socket_data_with_timeout(timeout_duration)
707            .await?;
708        if let WebInspectorEvent::SocketData { message, .. } = event {
709            self.observe_message(&message)?;
710            return Ok(message);
711        }
712        unreachable!("next_socket_data_with_timeout only returns socket-data events");
713    }
714
715    pub async fn send_command<S: AsyncRead + AsyncWrite + Unpin>(
716        &mut self,
717        client: &mut WebInspectorClient<S>,
718        method: &str,
719        params: JsonValue,
720    ) -> Result<u64, WebInspectorError> {
721        let params = match params {
722            JsonValue::Object(_) => params,
723            JsonValue::Null => JsonValue::Object(Default::default()),
724            other => {
725                return Err(WebInspectorError::Protocol(format!(
726                    "webinspector command params must be a JSON object, got {other}"
727                )))
728            }
729        };
730
731        let command_id = self.next_command_id;
732        self.next_command_id += 1;
733
734        let payload = if let Some(target_id) = &self.target_id {
735            let transport_id = self.next_transport_id;
736            self.next_transport_id += 1;
737            JsonValue::Object(serde_json::Map::from_iter([
738                ("id".to_string(), JsonValue::from(transport_id)),
739                (
740                    "method".to_string(),
741                    JsonValue::String("Target.sendMessageToTarget".to_string()),
742                ),
743                (
744                    "params".to_string(),
745                    JsonValue::Object(serde_json::Map::from_iter([
746                        ("targetId".to_string(), JsonValue::String(target_id.clone())),
747                        (
748                            "message".to_string(),
749                            JsonValue::String(serde_json::to_string(&JsonValue::Object(
750                                serde_json::Map::from_iter([
751                                    ("id".to_string(), JsonValue::from(command_id)),
752                                    ("method".to_string(), JsonValue::String(method.to_string())),
753                                    ("params".to_string(), params),
754                                ]),
755                            ))?),
756                        ),
757                    ])),
758                ),
759            ]))
760        } else {
761            JsonValue::Object(serde_json::Map::from_iter([
762                ("id".to_string(), JsonValue::from(command_id)),
763                ("method".to_string(), JsonValue::String(method.to_string())),
764                ("params".to_string(), params),
765            ]))
766        };
767
768        client
769            .send_socket_data(
770                &self.session_id,
771                &self.application_id,
772                self.page_id,
773                &payload,
774            )
775            .await?;
776        Ok(command_id)
777    }
778
779    pub async fn send_command_and_wait<S: AsyncRead + AsyncWrite + Unpin>(
780        &mut self,
781        client: &mut WebInspectorClient<S>,
782        method: &str,
783        params: JsonValue,
784        timeout_duration: Duration,
785    ) -> Result<JsonValue, WebInspectorError> {
786        let command_id = self.send_command(client, method, params).await?;
787        self.wait_for_response(client, command_id, timeout_duration)
788            .await
789    }
790
791    pub async fn send_bridge_message<S: AsyncRead + AsyncWrite + Unpin>(
792        &mut self,
793        client: &mut WebInspectorClient<S>,
794        message: &JsonValue,
795    ) -> Result<(), WebInspectorError> {
796        let payload = if let Some(target_id) = &self.target_id {
797            let transport_id = self.next_transport_id;
798            self.next_transport_id += 1;
799            json!({
800                "id": transport_id,
801                "method": "Target.sendMessageToTarget",
802                "params": {
803                    "targetId": target_id,
804                    "message": serde_json::to_string(message)?,
805                }
806            })
807        } else {
808            message.clone()
809        };
810
811        client
812            .send_socket_data(
813                &self.session_id,
814                &self.application_id,
815                self.page_id,
816                &payload,
817            )
818            .await
819    }
820
821    pub fn bridge_message(
822        &mut self,
823        message: &JsonValue,
824    ) -> Result<Option<JsonValue>, WebInspectorError> {
825        self.observe_message(message)?;
826        if self.target_id.is_some()
827            && message.get("id").is_some()
828            && message.get("method").is_none()
829        {
830            return Ok(None);
831        }
832        if message
833            .get("method")
834            .and_then(JsonValue::as_str)
835            .is_some_and(|method| method == "Target.dispatchMessageFromTarget")
836        {
837            let nested = message
838                .get("params")
839                .and_then(JsonValue::as_object)
840                .and_then(|params| params.get("message"))
841                .and_then(JsonValue::as_str)
842                .ok_or_else(|| {
843                    WebInspectorError::Protocol(
844                        "Target.dispatchMessageFromTarget missing params.message".to_string(),
845                    )
846                })?;
847            let nested: JsonValue = serde_json::from_str(nested)?;
848            self.observe_message(&nested)?;
849            return Ok(Some(nested));
850        }
851        Ok(Some(message.clone()))
852    }
853
854    async fn wait_for_target<S: AsyncRead + AsyncWrite + Unpin>(
855        &mut self,
856        client: &mut WebInspectorClient<S>,
857        timeout_duration: Duration,
858    ) -> Result<(), WebInspectorError> {
859        let deadline = Instant::now() + timeout_duration;
860        let mut skipped = Vec::new();
861        while self.target_id.is_none() {
862            let event = match client
863                .next_socket_data_with_timeout(remaining_time(deadline, timeout_duration)?)
864                .await
865            {
866                Ok(event) => event,
867                Err(error) => {
868                    client.restore_pending_events_front(skipped);
869                    return Err(error);
870                }
871            };
872            let WebInspectorEvent::SocketData { message, .. } = &event else {
873                unreachable!("next_socket_data_with_timeout only returns socket-data events");
874            };
875            self.observe_message(message)?;
876            if self.target_id.is_none() {
877                skipped.push(event);
878            }
879        }
880        client.restore_pending_events_front(skipped);
881        Ok(())
882    }
883
884    async fn wait_for_response<S: AsyncRead + AsyncWrite + Unpin>(
885        &mut self,
886        client: &mut WebInspectorClient<S>,
887        command_id: u64,
888        timeout_duration: Duration,
889    ) -> Result<JsonValue, WebInspectorError> {
890        let deadline = Instant::now() + timeout_duration;
891        let mut skipped = Vec::new();
892        loop {
893            let event = match client
894                .next_socket_data_with_timeout(remaining_time(deadline, timeout_duration)?)
895                .await
896            {
897                Ok(event) => event,
898                Err(error) => {
899                    client.restore_pending_events_front(skipped);
900                    return Err(error);
901                }
902            };
903            let WebInspectorEvent::SocketData { message, .. } = &event else {
904                unreachable!("next_socket_data_with_timeout only returns socket-data events");
905            };
906            match self.match_response(message, command_id) {
907                Ok(Some(response)) => {
908                    client.restore_pending_events_front(skipped);
909                    return Ok(response);
910                }
911                Ok(None) => {
912                    if self.should_preserve_message(message) {
913                        skipped.push(event);
914                    }
915                }
916                Err(error) => {
917                    client.restore_pending_events_front(skipped);
918                    return Err(error);
919                }
920            }
921        }
922    }
923
924    fn observe_message(&mut self, message: &JsonValue) -> Result<(), WebInspectorError> {
925        if message
926            .get("method")
927            .and_then(JsonValue::as_str)
928            .is_some_and(|method| method == "Target.targetCreated")
929        {
930            let target_id = message
931                .get("params")
932                .and_then(JsonValue::as_object)
933                .and_then(|params| params.get("targetInfo"))
934                .and_then(JsonValue::as_object)
935                .and_then(|info| info.get("targetId"))
936                .and_then(JsonValue::as_str)
937                .ok_or_else(|| {
938                    WebInspectorError::Protocol(
939                        "Target.targetCreated missing params.targetInfo.targetId".to_string(),
940                    )
941                })?;
942            self.target_id = Some(target_id.to_string());
943        }
944
945        if message
946            .get("method")
947            .and_then(JsonValue::as_str)
948            .is_some_and(|method| method == "Target.targetDestroyed")
949        {
950            if let Some(target_id) = message
951                .get("params")
952                .and_then(JsonValue::as_object)
953                .and_then(|params| params.get("targetId"))
954                .and_then(JsonValue::as_str)
955            {
956                if self.target_id.as_deref() == Some(target_id) {
957                    self.target_id = None;
958                }
959            }
960        }
961
962        if message
963            .get("method")
964            .and_then(JsonValue::as_str)
965            .is_some_and(|method| method == "Target.didCommitProvisionalTarget")
966        {
967            let target_id = message
968                .get("params")
969                .and_then(JsonValue::as_object)
970                .and_then(|params| params.get("newTargetId"))
971                .and_then(JsonValue::as_str)
972                .ok_or_else(|| {
973                    WebInspectorError::Protocol(
974                        "Target.didCommitProvisionalTarget missing params.newTargetId".to_string(),
975                    )
976                })?;
977            self.target_id = Some(target_id.to_string());
978        }
979
980        Ok(())
981    }
982
983    fn match_response(
984        &mut self,
985        message: &JsonValue,
986        command_id: u64,
987    ) -> Result<Option<JsonValue>, WebInspectorError> {
988        if self.target_id.is_none()
989            && message
990                .get("id")
991                .and_then(JsonValue::as_u64)
992                .is_some_and(|id| id == command_id)
993        {
994            return Ok(Some(message.clone()));
995        }
996
997        if message
998            .get("method")
999            .and_then(JsonValue::as_str)
1000            .is_some_and(|method| method == "Target.dispatchMessageFromTarget")
1001        {
1002            let nested = message
1003                .get("params")
1004                .and_then(JsonValue::as_object)
1005                .and_then(|params| params.get("message"))
1006                .and_then(JsonValue::as_str)
1007                .ok_or_else(|| {
1008                    WebInspectorError::Protocol(
1009                        "Target.dispatchMessageFromTarget missing params.message".to_string(),
1010                    )
1011                })?;
1012            let nested: JsonValue = serde_json::from_str(nested)?;
1013            self.observe_message(&nested)?;
1014            if nested
1015                .get("id")
1016                .and_then(JsonValue::as_u64)
1017                .is_some_and(|id| id == command_id)
1018            {
1019                return Ok(Some(nested));
1020            }
1021        }
1022
1023        Ok(None)
1024    }
1025
1026    fn should_preserve_message(&self, message: &JsonValue) -> bool {
1027        !(self.target_id.is_some()
1028            && message.get("id").is_some()
1029            && message.get("method").is_none())
1030    }
1031}
1032
1033#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1034#[serde(rename_all = "snake_case")]
1035pub enum By {
1036    Id,
1037    XPath,
1038    LinkText,
1039    PartialLinkText,
1040    Name,
1041    TagName,
1042    ClassName,
1043    CssSelector,
1044}
1045
1046impl By {
1047    fn as_wire(self) -> &'static str {
1048        match self {
1049            Self::Id => "id",
1050            Self::XPath => "xpath",
1051            Self::LinkText => "link text",
1052            Self::PartialLinkText => "partial link text",
1053            Self::Name => "name",
1054            Self::TagName => "tag name",
1055            Self::ClassName => "class name",
1056            Self::CssSelector => "css selector",
1057        }
1058    }
1059}
1060
1061const FIND_NODES_JS: &str = r#"function(strategy,ancestorElement,query,firstResultOnly,timeoutDuration,callback){ancestorElement=ancestorElement||document;switch(strategy){case"id":strategy="css selector";query="[id=\""+escape(query)+"\"]";break;case"name":strategy="css selector";query="[name=\""+escape(query)+"\"]";break;}switch(strategy){case"css selector":case"link text":case"partial link text":case"tag name":case"class name":case"xpath":break;default: throw{name:"InvalidParameter",message:("Unsupported locator strategy: "+strategy+".")};}function escape(string){return string.replace(/\\/g,"\\\\").replace(/"/g,"\\\"");}function tryToFindNode(){try{switch(strategy){case"css selector":if(firstResultOnly)return ancestorElement.querySelector(query)||null;return Array.from(ancestorElement.querySelectorAll(query));case"link text":let linkTextResult=[];for(let link of ancestorElement.getElementsByTagName("a")){if(link.text.trim()==query){linkTextResult.push(link);if(firstResultOnly)break;}}if(firstResultOnly)return linkTextResult[0]||null;return linkTextResult;case"partial link text":let partialLinkResult=[];for(let link of ancestorElement.getElementsByTagName("a")){if(link.text.includes(query)){partialLinkResult.push(link);if(firstResultOnly)break;}}if(firstResultOnly)return partialLinkResult[0]||null;return partialLinkResult;case"tag name":let tagNameResult=ancestorElement.getElementsByTagName(query);if(firstResultOnly)return tagNameResult[0]||null;return Array.from(tagNameResult);case"class name":let classNameResult=ancestorElement.getElementsByClassName(query);if(firstResultOnly)return classNameResult[0]||null;return Array.from(classNameResult);case"xpath":if(firstResultOnly){let xpathResult=document.evaluate(query,ancestorElement,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null);if(!xpathResult)return null;return xpathResult.singleNodeValue;}let xpathResult=document.evaluate(query,ancestorElement,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);if(!xpathResult||!xpathResult.snapshotLength)return[];let arrayResult=[];for(let i=0;i<xpathResult.snapshotLength;++i)arrayResult.push(xpathResult.snapshotItem(i));return arrayResult;}}catch(error){ throw{name:"InvalidSelector",message:error.message};}}const pollInterval=50;let pollUntil=performance.now()+timeoutDuration;function pollForNode(){let result=tryToFindNode();if(typeof result==="string"||result instanceof Node||(result instanceof Array&&result.length)){callback(result);return;}let durationRemaining=pollUntil-performance.now();if(durationRemaining<pollInterval){callback(firstResultOnly?null:[]);return;}setTimeout(pollForNode,pollInterval);}pollForNode();}"#;
1062const CLICK_ELEMENT_JS: &str = r#"function(element) { element.click(); return null; }"#;
1063const ELEMENT_TEXT_JS: &str =
1064    r#"function(element) { return element.innerText.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g, ""); }"#;
1065const ELEMENT_TAG_JS: &str = r#"function(element) { return element.tagName.toLowerCase(); }"#;
1066
1067#[derive(Debug, Clone)]
1068pub struct AutomationSession {
1069    application_id: String,
1070    bundle_identifier: String,
1071    session_id: String,
1072    page_id: Option<u64>,
1073    top_level_handle: Option<String>,
1074    implicit_wait_timeout_ms: u64,
1075    page_load_timeout_ms: u64,
1076    next_command_id: u64,
1077}
1078
1079impl AutomationSession {
1080    pub fn new(application_id: impl Into<String>, bundle_identifier: impl Into<String>) -> Self {
1081        Self::with_session_id(
1082            application_id,
1083            bundle_identifier,
1084            Uuid::new_v4().to_string().to_uppercase(),
1085        )
1086    }
1087
1088    pub fn with_session_id(
1089        application_id: impl Into<String>,
1090        bundle_identifier: impl Into<String>,
1091        session_id: impl Into<String>,
1092    ) -> Self {
1093        Self {
1094            application_id: application_id.into(),
1095            bundle_identifier: bundle_identifier.into(),
1096            session_id: session_id.into(),
1097            page_id: None,
1098            top_level_handle: None,
1099            implicit_wait_timeout_ms: 0,
1100            page_load_timeout_ms: 3_000_000,
1101            next_command_id: 1,
1102        }
1103    }
1104
1105    pub fn with_page(
1106        application_id: impl Into<String>,
1107        bundle_identifier: impl Into<String>,
1108        session_id: impl Into<String>,
1109        page_id: u64,
1110    ) -> Self {
1111        let mut session = Self::with_session_id(application_id, bundle_identifier, session_id);
1112        session.page_id = Some(page_id);
1113        session.top_level_handle = Some(String::new());
1114        session
1115    }
1116
1117    pub fn session_id(&self) -> &str {
1118        &self.session_id
1119    }
1120
1121    pub fn bundle_identifier(&self) -> &str {
1122        &self.bundle_identifier
1123    }
1124
1125    pub fn page_id(&self) -> u64 {
1126        self.page_id.unwrap_or_default()
1127    }
1128
1129    pub fn set_implicit_wait_timeout(&mut self, timeout: Duration) {
1130        self.implicit_wait_timeout_ms = timeout.as_millis() as u64;
1131    }
1132
1133    pub async fn attach<S: AsyncRead + AsyncWrite + Unpin>(
1134        &mut self,
1135        client: &mut WebInspectorClient<S>,
1136        timeout_duration: Duration,
1137    ) -> Result<(), WebInspectorError> {
1138        if matches!(
1139            client.automation_availability(),
1140            Some(AutomationAvailability::NotAvailable)
1141        ) {
1142            return Err(WebInspectorError::Protocol(
1143                "remote automation is not available".to_string(),
1144            ));
1145        }
1146        client
1147            .request_automation_session(&self.session_id, &self.application_id)
1148            .await?;
1149        client.request_listing(&self.application_id).await?;
1150
1151        let page = self
1152            .wait_for_automation_page(client, timeout_duration, false)
1153            .await?;
1154        self.page_id = Some(page.id);
1155
1156        client
1157            .send_socket_setup(&self.session_id, &self.application_id, page.id, true)
1158            .await?;
1159        client.request_listing(&self.application_id).await?;
1160        let page = self
1161            .wait_for_automation_page(client, timeout_duration, true)
1162            .await?;
1163        self.page_id = Some(page.id);
1164        Ok(())
1165    }
1166
1167    pub async fn start_session<S: AsyncRead + AsyncWrite + Unpin>(
1168        &mut self,
1169        client: &mut WebInspectorClient<S>,
1170    ) -> Result<String, WebInspectorError> {
1171        let response = self
1172            .send_command_and_wait(
1173                client,
1174                "createBrowsingContext",
1175                JsonValue::Object(Default::default()),
1176                Duration::from_secs(10),
1177            )
1178            .await?;
1179        let handle = response
1180            .get("handle")
1181            .and_then(JsonValue::as_str)
1182            .ok_or_else(|| {
1183                WebInspectorError::Protocol(
1184                    "Automation.createBrowsingContext missing result.handle".to_string(),
1185                )
1186            })?
1187            .to_string();
1188        self.top_level_handle = Some(handle.clone());
1189        Ok(handle)
1190    }
1191
1192    pub async fn stop_session<S: AsyncRead + AsyncWrite + Unpin>(
1193        &mut self,
1194        client: &mut WebInspectorClient<S>,
1195    ) -> Result<(), WebInspectorError> {
1196        let Some(handle) = self.top_level_handle.clone() else {
1197            return Ok(());
1198        };
1199        let _ = self
1200            .send_command_and_wait(
1201                client,
1202                "closeBrowsingContext",
1203                json!({ "handle": handle }),
1204                Duration::from_secs(10),
1205            )
1206            .await?;
1207        self.top_level_handle = None;
1208        Ok(())
1209    }
1210
1211    pub async fn navigate<S: AsyncRead + AsyncWrite + Unpin>(
1212        &mut self,
1213        client: &mut WebInspectorClient<S>,
1214        url: &str,
1215    ) -> Result<(), WebInspectorError> {
1216        let handle = self.require_top_level_handle()?;
1217        let _ = self
1218            .send_command_and_wait(
1219                client,
1220                "navigateBrowsingContext",
1221                json!({
1222                    "handle": handle,
1223                    "pageLoadTimeout": self.page_load_timeout_ms,
1224                    "url": url,
1225                }),
1226                Duration::from_secs(10),
1227            )
1228            .await?;
1229        Ok(())
1230    }
1231
1232    pub async fn go_back<S: AsyncRead + AsyncWrite + Unpin>(
1233        &mut self,
1234        client: &mut WebInspectorClient<S>,
1235    ) -> Result<(), WebInspectorError> {
1236        let handle = self.require_top_level_handle()?;
1237        let _ = self
1238            .send_command_and_wait(
1239                client,
1240                "goBackInBrowsingContext",
1241                json!({
1242                    "handle": handle,
1243                    "pageLoadTimeout": self.page_load_timeout_ms,
1244                }),
1245                Duration::from_secs(10),
1246            )
1247            .await?;
1248        Ok(())
1249    }
1250
1251    pub async fn go_forward<S: AsyncRead + AsyncWrite + Unpin>(
1252        &mut self,
1253        client: &mut WebInspectorClient<S>,
1254    ) -> Result<(), WebInspectorError> {
1255        let handle = self.require_top_level_handle()?;
1256        let _ = self
1257            .send_command_and_wait(
1258                client,
1259                "goForwardInBrowsingContext",
1260                json!({
1261                    "handle": handle,
1262                    "pageLoadTimeout": self.page_load_timeout_ms,
1263                }),
1264                Duration::from_secs(10),
1265            )
1266            .await?;
1267        Ok(())
1268    }
1269
1270    pub async fn refresh<S: AsyncRead + AsyncWrite + Unpin>(
1271        &mut self,
1272        client: &mut WebInspectorClient<S>,
1273    ) -> Result<(), WebInspectorError> {
1274        let handle = self.require_top_level_handle()?;
1275        let _ = self
1276            .send_command_and_wait(
1277                client,
1278                "reloadBrowsingContext",
1279                json!({
1280                    "handle": handle,
1281                    "pageLoadTimeout": self.page_load_timeout_ms,
1282                }),
1283                Duration::from_secs(10),
1284            )
1285            .await?;
1286        Ok(())
1287    }
1288
1289    pub async fn current_url<S: AsyncRead + AsyncWrite + Unpin>(
1290        &mut self,
1291        client: &mut WebInspectorClient<S>,
1292    ) -> Result<Option<String>, WebInspectorError> {
1293        let context = self.get_browsing_context(client).await?;
1294        Ok(context
1295            .get("url")
1296            .and_then(JsonValue::as_str)
1297            .map(ToOwned::to_owned))
1298    }
1299
1300    pub async fn execute_script<S: AsyncRead + AsyncWrite + Unpin>(
1301        &mut self,
1302        client: &mut WebInspectorClient<S>,
1303        script: &str,
1304        args: &[JsonValue],
1305    ) -> Result<JsonValue, WebInspectorError> {
1306        let handle = self.require_top_level_handle()?;
1307        let response = self
1308            .send_command_and_wait(
1309                client,
1310                "evaluateJavaScriptFunction",
1311                json!({
1312                    "browsingContextHandle": handle,
1313                    "function": format!("function(){{\n{script}\n}}"),
1314                    "arguments": args.iter().map(stringify_automation_argument).collect::<Result<Vec<_>, _>>()?,
1315                }),
1316                Duration::from_secs(10),
1317            )
1318            .await?;
1319        decode_automation_result(&response)
1320    }
1321
1322    pub async fn evaluate_js_function<S: AsyncRead + AsyncWrite + Unpin>(
1323        &mut self,
1324        client: &mut WebInspectorClient<S>,
1325        function: &str,
1326        args: &[JsonValue],
1327        implicit_callback: bool,
1328    ) -> Result<JsonValue, WebInspectorError> {
1329        let handle = self.require_top_level_handle()?;
1330        let mut params = serde_json::Map::from_iter([
1331            (
1332                "browsingContextHandle".to_string(),
1333                JsonValue::String(handle),
1334            ),
1335            (
1336                "function".to_string(),
1337                JsonValue::String(function.to_string()),
1338            ),
1339            (
1340                "arguments".to_string(),
1341                JsonValue::Array(
1342                    args.iter()
1343                        .map(stringify_automation_argument)
1344                        .collect::<Result<Vec<_>, _>>()?,
1345                ),
1346            ),
1347        ]);
1348        if implicit_callback {
1349            params.insert(
1350                "expectsImplicitCallbackArgument".to_string(),
1351                JsonValue::Bool(true),
1352            );
1353            if self.implicit_wait_timeout_ms > 0 {
1354                params.insert(
1355                    "callbackTimeout".to_string(),
1356                    JsonValue::from(self.implicit_wait_timeout_ms + 1_000),
1357                );
1358            }
1359        }
1360        let response = self
1361            .send_command_and_wait(
1362                client,
1363                "evaluateJavaScriptFunction",
1364                JsonValue::Object(params),
1365                Duration::from_secs(10),
1366            )
1367            .await?;
1368        decode_automation_result(&response)
1369    }
1370
1371    pub async fn get_title<S: AsyncRead + AsyncWrite + Unpin>(
1372        &mut self,
1373        client: &mut WebInspectorClient<S>,
1374    ) -> Result<String, WebInspectorError> {
1375        Ok(self
1376            .evaluate_js_function(client, "function() { return document.title; }", &[], false)
1377            .await?
1378            .as_str()
1379            .unwrap_or_default()
1380            .to_string())
1381    }
1382
1383    pub async fn get_page_source<S: AsyncRead + AsyncWrite + Unpin>(
1384        &mut self,
1385        client: &mut WebInspectorClient<S>,
1386    ) -> Result<String, WebInspectorError> {
1387        Ok(self
1388            .evaluate_js_function(
1389                client,
1390                "function() { return document.documentElement.outerHTML; }",
1391                &[],
1392                false,
1393            )
1394            .await?
1395            .as_str()
1396            .unwrap_or_default()
1397            .to_string())
1398    }
1399
1400    pub async fn screenshot_base64<S: AsyncRead + AsyncWrite + Unpin>(
1401        &mut self,
1402        client: &mut WebInspectorClient<S>,
1403    ) -> Result<String, WebInspectorError> {
1404        let handle = self.require_top_level_handle()?;
1405        let response = self
1406            .send_command_and_wait(
1407                client,
1408                "takeScreenshot",
1409                json!({
1410                    "handle": handle,
1411                    "clipToViewport": true,
1412                }),
1413                Duration::from_secs(10),
1414            )
1415            .await?;
1416        response
1417            .get("data")
1418            .and_then(JsonValue::as_str)
1419            .map(ToOwned::to_owned)
1420            .ok_or_else(|| {
1421                WebInspectorError::Protocol(
1422                    "Automation.takeScreenshot missing result.data".to_string(),
1423                )
1424            })
1425    }
1426
1427    pub async fn find_element<S: AsyncRead + AsyncWrite + Unpin>(
1428        &mut self,
1429        client: &mut WebInspectorClient<S>,
1430        by: By,
1431        value: &str,
1432    ) -> Result<Option<JsonValue>, WebInspectorError> {
1433        Ok(self
1434            .find_elements_internal(client, by, value, true, None)
1435            .await?
1436            .into_iter()
1437            .next())
1438    }
1439
1440    pub async fn find_elements<S: AsyncRead + AsyncWrite + Unpin>(
1441        &mut self,
1442        client: &mut WebInspectorClient<S>,
1443        by: By,
1444        value: &str,
1445        single: bool,
1446    ) -> Result<Vec<JsonValue>, WebInspectorError> {
1447        self.find_elements_internal(client, by, value, single, None)
1448            .await
1449    }
1450
1451    pub async fn click_element<S: AsyncRead + AsyncWrite + Unpin>(
1452        &mut self,
1453        client: &mut WebInspectorClient<S>,
1454        element: &JsonValue,
1455    ) -> Result<(), WebInspectorError> {
1456        let _ = self
1457            .evaluate_js_function(
1458                client,
1459                CLICK_ELEMENT_JS,
1460                std::slice::from_ref(element),
1461                false,
1462            )
1463            .await?;
1464        Ok(())
1465    }
1466
1467    pub async fn element_text<S: AsyncRead + AsyncWrite + Unpin>(
1468        &mut self,
1469        client: &mut WebInspectorClient<S>,
1470        element: &JsonValue,
1471    ) -> Result<String, WebInspectorError> {
1472        Ok(self
1473            .evaluate_js_function(
1474                client,
1475                ELEMENT_TEXT_JS,
1476                std::slice::from_ref(element),
1477                false,
1478            )
1479            .await?
1480            .as_str()
1481            .unwrap_or_default()
1482            .to_string())
1483    }
1484
1485    pub async fn element_tag_name<S: AsyncRead + AsyncWrite + Unpin>(
1486        &mut self,
1487        client: &mut WebInspectorClient<S>,
1488        element: &JsonValue,
1489    ) -> Result<String, WebInspectorError> {
1490        Ok(self
1491            .evaluate_js_function(client, ELEMENT_TAG_JS, std::slice::from_ref(element), false)
1492            .await?
1493            .as_str()
1494            .unwrap_or_default()
1495            .to_string())
1496    }
1497
1498    async fn get_browsing_context<S: AsyncRead + AsyncWrite + Unpin>(
1499        &mut self,
1500        client: &mut WebInspectorClient<S>,
1501    ) -> Result<JsonValue, WebInspectorError> {
1502        let handle = self.require_top_level_handle()?;
1503        let response = self
1504            .send_command_and_wait(
1505                client,
1506                "getBrowsingContext",
1507                json!({ "handle": handle }),
1508                Duration::from_secs(10),
1509            )
1510            .await?;
1511        Ok(response
1512            .get("context")
1513            .cloned()
1514            .unwrap_or(JsonValue::Object(Default::default())))
1515    }
1516
1517    async fn find_elements_internal<S: AsyncRead + AsyncWrite + Unpin>(
1518        &mut self,
1519        client: &mut WebInspectorClient<S>,
1520        by: By,
1521        value: &str,
1522        single: bool,
1523        root: Option<JsonValue>,
1524    ) -> Result<Vec<JsonValue>, WebInspectorError> {
1525        let (strategy, query) = normalized_locator(by, value);
1526        let response = self
1527            .evaluate_js_function(
1528                client,
1529                FIND_NODES_JS,
1530                &[
1531                    JsonValue::String(strategy),
1532                    root.unwrap_or(JsonValue::Null),
1533                    JsonValue::String(query),
1534                    JsonValue::Bool(single),
1535                    JsonValue::from(self.implicit_wait_timeout_ms),
1536                ],
1537                true,
1538            )
1539            .await?;
1540
1541        Ok(match response {
1542            JsonValue::Null => Vec::new(),
1543            JsonValue::Array(values) => values,
1544            other => vec![other],
1545        })
1546    }
1547
1548    async fn send_command_and_wait<S: AsyncRead + AsyncWrite + Unpin>(
1549        &mut self,
1550        client: &mut WebInspectorClient<S>,
1551        method: &str,
1552        params: JsonValue,
1553        timeout_duration: Duration,
1554    ) -> Result<JsonValue, WebInspectorError> {
1555        let command_id = self.send_command(client, method, params).await?;
1556        self.wait_for_response(client, command_id, timeout_duration)
1557            .await
1558    }
1559
1560    async fn send_command<S: AsyncRead + AsyncWrite + Unpin>(
1561        &mut self,
1562        client: &mut WebInspectorClient<S>,
1563        method: &str,
1564        params: JsonValue,
1565    ) -> Result<u64, WebInspectorError> {
1566        let page_id = self.page_id.ok_or_else(|| {
1567            WebInspectorError::Protocol("automation session has not attached to a page".to_string())
1568        })?;
1569        let command_id = self.next_command_id;
1570        self.next_command_id += 1;
1571        client
1572            .send_socket_data(
1573                &self.session_id,
1574                &self.application_id,
1575                page_id,
1576                &json!({
1577                    "id": command_id,
1578                    "method": format!("Automation.{method}"),
1579                    "params": params,
1580                }),
1581            )
1582            .await?;
1583        Ok(command_id)
1584    }
1585
1586    async fn wait_for_response<S: AsyncRead + AsyncWrite + Unpin>(
1587        &mut self,
1588        client: &mut WebInspectorClient<S>,
1589        command_id: u64,
1590        timeout_duration: Duration,
1591    ) -> Result<JsonValue, WebInspectorError> {
1592        let deadline = Instant::now() + timeout_duration;
1593        let mut skipped = Vec::new();
1594        loop {
1595            let event = match client
1596                .next_socket_data_with_timeout(remaining_time(deadline, timeout_duration)?)
1597                .await
1598            {
1599                Ok(event) => event,
1600                Err(error) => {
1601                    client.restore_pending_events_front(skipped);
1602                    return Err(error);
1603                }
1604            };
1605            if let WebInspectorEvent::SocketData { message, .. } = &event {
1606                if message.get("id").and_then(JsonValue::as_u64) == Some(command_id) {
1607                    if let Some(error) = message.get("error") {
1608                        client.restore_pending_events_front(skipped);
1609                        return Err(WebInspectorError::Protocol(format!(
1610                            "automation command failed: {error}"
1611                        )));
1612                    }
1613                    client.restore_pending_events_front(skipped);
1614                    return Ok(message
1615                        .get("result")
1616                        .cloned()
1617                        .unwrap_or(JsonValue::Object(Default::default())));
1618                }
1619            }
1620            skipped.push(event);
1621        }
1622    }
1623
1624    async fn wait_for_automation_page<S: AsyncRead + AsyncWrite + Unpin>(
1625        &self,
1626        client: &mut WebInspectorClient<S>,
1627        timeout_duration: Duration,
1628        require_connection_id: bool,
1629    ) -> Result<Page, WebInspectorError> {
1630        let deadline = Instant::now() + timeout_duration;
1631        loop {
1632            if let Some(page) =
1633                client.automation_page_by_session(&self.application_id, &self.session_id)
1634            {
1635                if !require_connection_id || page.automation_connection_id.is_some() {
1636                    return Ok(page.clone());
1637                }
1638            }
1639            let _ = client
1640                .next_event_with_timeout(remaining_time(deadline, timeout_duration)?)
1641                .await?;
1642        }
1643    }
1644
1645    fn require_top_level_handle(&self) -> Result<String, WebInspectorError> {
1646        self.top_level_handle.clone().ok_or_else(|| {
1647            WebInspectorError::Protocol(
1648                "automation session has not started a browsing context".to_string(),
1649            )
1650        })
1651    }
1652}
1653
1654fn stringify_automation_argument(value: &JsonValue) -> Result<JsonValue, WebInspectorError> {
1655    Ok(JsonValue::String(serde_json::to_string(value)?))
1656}
1657
1658fn decode_automation_result(value: &JsonValue) -> Result<JsonValue, WebInspectorError> {
1659    match value.get("result") {
1660        Some(JsonValue::String(result)) => Ok(serde_json::from_str(result)?),
1661        Some(other) => Ok(other.clone()),
1662        None => Ok(JsonValue::Null),
1663    }
1664}
1665
1666fn normalized_locator(by: By, value: &str) -> (String, String) {
1667    match by {
1668        By::Id => ("css selector".to_string(), format!("[id=\"{value}\"]")),
1669        By::Name => ("css selector".to_string(), format!("[name=\"{value}\"]")),
1670        By::ClassName => ("css selector".to_string(), format!(".{value}")),
1671        By::TagName => ("css selector".to_string(), value.to_string()),
1672        _ => (by.as_wire().to_string(), value.to_string()),
1673    }
1674}
1675
1676async fn send_plist<S: AsyncWrite + Unpin>(
1677    stream: &mut S,
1678    value: &plist::Value,
1679) -> Result<(), WebInspectorError> {
1680    let mut payload = Vec::new();
1681    plist::to_writer_xml(&mut payload, value)?;
1682    stream
1683        .write_all(&(payload.len() as u32).to_be_bytes())
1684        .await?;
1685    stream.write_all(&payload).await?;
1686    stream.flush().await?;
1687    Ok(())
1688}
1689
1690async fn recv_plist<S: AsyncRead + Unpin>(
1691    stream: &mut S,
1692) -> Result<plist::Dictionary, WebInspectorError> {
1693    let mut len_buf = [0u8; 4];
1694    stream.read_exact(&mut len_buf).await?;
1695    let len = u32::from_be_bytes(len_buf) as usize;
1696    if len > MAX_PLIST_SIZE {
1697        return Err(WebInspectorError::Protocol(format!(
1698            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
1699        )));
1700    }
1701    let mut payload = vec![0u8; len];
1702    stream.read_exact(&mut payload).await?;
1703    Ok(plist::from_bytes(&payload)?)
1704}
1705
1706fn required_string<'a>(
1707    dict: &'a plist::Dictionary,
1708    key: &str,
1709) -> Result<&'a str, WebInspectorError> {
1710    dict.get(key)
1711        .and_then(plist::Value::as_string)
1712        .ok_or_else(|| WebInspectorError::Protocol(format!("missing string field '{key}'")))
1713}
1714
1715fn optional_string<'a>(dict: &'a plist::Dictionary, key: &str) -> Option<&'a str> {
1716    dict.get(key).and_then(plist::Value::as_string)
1717}
1718
1719fn required_bool(dict: &plist::Dictionary, key: &str) -> Result<bool, WebInspectorError> {
1720    optional_bool(dict, key)
1721        .ok_or_else(|| WebInspectorError::Protocol(format!("missing bool field '{key}'")))
1722}
1723
1724fn optional_bool(dict: &plist::Dictionary, key: &str) -> Option<bool> {
1725    match dict.get(key) {
1726        Some(plist::Value::Boolean(value)) => Some(*value),
1727        Some(plist::Value::Integer(value)) => value
1728            .as_unsigned()
1729            .map(|value| value != 0)
1730            .or_else(|| value.as_signed().map(|value| value != 0)),
1731        _ => None,
1732    }
1733}
1734
1735fn plist_integer_to_u64(value: &plist::Value) -> Option<u64> {
1736    match value {
1737        plist::Value::Integer(value) => value
1738            .as_unsigned()
1739            .or_else(|| value.as_signed().map(|value| value as u64)),
1740        _ => None,
1741    }
1742}
1743
1744fn pid_from_identifier(identifier: &str) -> Result<u64, WebInspectorError> {
1745    identifier
1746        .rsplit(':')
1747        .next()
1748        .ok_or_else(|| {
1749            WebInspectorError::Protocol(format!(
1750                "application identifier '{identifier}' does not contain ':'"
1751            ))
1752        })?
1753        .parse::<u64>()
1754        .map_err(|error| {
1755            WebInspectorError::Protocol(format!(
1756                "failed to parse PID from identifier '{identifier}': {error}"
1757            ))
1758        })
1759}
1760
1761fn extract_json_payload(
1762    dict: &plist::Dictionary,
1763    key: &str,
1764) -> Result<JsonValue, WebInspectorError> {
1765    match dict.get(key) {
1766        Some(plist::Value::Data(payload)) => Ok(serde_json::from_slice(payload)?),
1767        Some(plist::Value::String(payload)) => Ok(serde_json::from_str(payload)?),
1768        Some(other) => Err(WebInspectorError::Protocol(format!(
1769            "{key} expected data/string payload, got {other:?}"
1770        ))),
1771        None => Err(WebInspectorError::Protocol(format!(
1772            "missing JSON payload field '{key}'"
1773        ))),
1774    }
1775}
1776
1777fn remaining_time(deadline: Instant, fallback: Duration) -> Result<Duration, WebInspectorError> {
1778    let now = Instant::now();
1779    if now >= deadline {
1780        return Err(WebInspectorError::Timeout(fallback));
1781    }
1782    Ok(deadline.duration_since(now))
1783}
1784
1785#[cfg(test)]
1786mod tests {
1787    use indexmap::IndexMap;
1788    use tokio::io::{duplex, AsyncWriteExt};
1789
1790    use super::*;
1791
1792    fn encode_plist(value: &plist::Value) -> Vec<u8> {
1793        let mut payload = Vec::new();
1794        plist::to_writer_xml(&mut payload, value).expect("plist serialization");
1795        let mut framed = Vec::with_capacity(payload.len() + 4);
1796        framed.extend_from_slice(&(payload.len() as u32).to_be_bytes());
1797        framed.extend_from_slice(&payload);
1798        framed
1799    }
1800
1801    #[test]
1802    fn open_pages_snapshot_only_includes_pages_for_connected_apps() {
1803        let stream = tokio::io::empty();
1804        let mut client = WebInspectorClient::with_connection_id(stream, "TEST");
1805        client.applications.insert(
1806            "PID:42".into(),
1807            Application {
1808                id: "PID:42".into(),
1809                bundle_identifier: "com.apple.mobilesafari".into(),
1810                pid: 42,
1811                name: "Safari".into(),
1812                availability: AutomationAvailability::Available,
1813                is_active: true,
1814                is_proxy: false,
1815                is_ready: true,
1816                host_application_identifier: None,
1817            },
1818        );
1819        client.application_pages.insert(
1820            "PID:42".into(),
1821            IndexMap::from_iter([(
1822                7,
1823                Page {
1824                    id: 7,
1825                    listing_key: "page-7".into(),
1826                    page_type: WirType::WebPage,
1827                    title: Some("Example".into()),
1828                    url: Some("https://example.com".into()),
1829                    automation_is_paired: None,
1830                    automation_name: None,
1831                    automation_version: None,
1832                    automation_session_id: None,
1833                    automation_connection_id: None,
1834                },
1835            )]),
1836        );
1837        client.application_pages.insert(
1838            "PID:99".into(),
1839            IndexMap::from_iter([(
1840                9,
1841                Page {
1842                    id: 9,
1843                    listing_key: "orphan".into(),
1844                    page_type: WirType::WebPage,
1845                    title: None,
1846                    url: None,
1847                    automation_is_paired: None,
1848                    automation_name: None,
1849                    automation_version: None,
1850                    automation_session_id: None,
1851                    automation_connection_id: None,
1852                },
1853            )]),
1854        );
1855
1856        let snapshot = client.open_pages_snapshot();
1857        assert_eq!(snapshot.len(), 1);
1858        assert_eq!(snapshot[0].application.id, "PID:42");
1859        assert_eq!(snapshot[0].page.id, 7);
1860    }
1861
1862    #[test]
1863    fn extract_json_payload_accepts_string_payload() {
1864        let dict = plist::Dictionary::from_iter([(
1865            "WIRMessageDataKey".to_string(),
1866            plist::Value::String("{\"id\":1}".into()),
1867        )]);
1868
1869        assert_eq!(
1870            extract_json_payload(&dict, "WIRMessageDataKey").unwrap(),
1871            json!({ "id": 1 })
1872        );
1873    }
1874
1875    #[test]
1876    fn pid_from_identifier_rejects_non_numeric_suffix() {
1877        let err = pid_from_identifier("PID:not-a-number")
1878            .expect_err("invalid pid suffix must return an error");
1879        assert!(err
1880            .to_string()
1881            .contains("failed to parse PID from identifier 'PID:not-a-number'"));
1882    }
1883
1884    #[test]
1885    fn normalized_locator_rewrites_common_dom_strategies() {
1886        assert_eq!(
1887            normalized_locator(By::ClassName, "hero"),
1888            ("css selector".into(), ".hero".into())
1889        );
1890        assert_eq!(
1891            normalized_locator(By::Id, "main"),
1892            ("css selector".into(), "[id=\"main\"]".into())
1893        );
1894        assert_eq!(
1895            normalized_locator(By::TagName, "button"),
1896            ("css selector".into(), "button".into())
1897        );
1898    }
1899
1900    #[test]
1901    fn remaining_time_errors_after_deadline() {
1902        let fallback = Duration::from_millis(25);
1903        let err = remaining_time(Instant::now(), fallback)
1904            .expect_err("expired deadlines must become timeout errors");
1905        assert!(matches!(err, WebInspectorError::Timeout(duration) if duration == fallback));
1906    }
1907
1908    #[allow(clippy::type_complexity)]
1909    fn application_listing_message(
1910        application_id: &str,
1911        pages: &[(&str, u64, &str, Option<&str>, Option<&str>)],
1912    ) -> plist::Dictionary {
1913        let listing = pages
1914            .iter()
1915            .map(|(listing_key, page_id, page_type, title, url)| {
1916                let mut page = plist::Dictionary::from_iter([
1917                    (
1918                        "WIRPageIdentifierKey".to_string(),
1919                        plist::Value::Integer((*page_id).into()),
1920                    ),
1921                    (
1922                        "WIRTypeKey".to_string(),
1923                        plist::Value::String((*page_type).to_string()),
1924                    ),
1925                ]);
1926                if let Some(title) = title {
1927                    page.insert(
1928                        "WIRTitleKey".to_string(),
1929                        plist::Value::String((*title).to_string()),
1930                    );
1931                }
1932                if let Some(url) = url {
1933                    page.insert(
1934                        "WIRURLKey".to_string(),
1935                        plist::Value::String((*url).to_string()),
1936                    );
1937                }
1938                ((*listing_key).to_string(), plist::Value::Dictionary(page))
1939            });
1940
1941        plist::Dictionary::from_iter([
1942            (
1943                "__selector".to_string(),
1944                plist::Value::String("_rpc_applicationSentListing:".into()),
1945            ),
1946            (
1947                "__argument".to_string(),
1948                plist::Value::Dictionary(plist::Dictionary::from_iter([
1949                    (
1950                        "WIRApplicationIdentifierKey".to_string(),
1951                        plist::Value::String(application_id.to_string()),
1952                    ),
1953                    (
1954                        "WIRListingKey".to_string(),
1955                        plist::Value::Dictionary(plist::Dictionary::from_iter(listing)),
1956                    ),
1957                ])),
1958            ),
1959        ])
1960    }
1961
1962    #[tokio::test]
1963    async fn recv_plist_rejects_oversized_frames() {
1964        let (client, mut server) = duplex(64);
1965        let task = tokio::spawn(async move {
1966            let mut stream = client;
1967            recv_plist(&mut stream).await
1968        });
1969
1970        server
1971            .write_all(&((MAX_PLIST_SIZE as u32) + 1).to_be_bytes())
1972            .await
1973            .unwrap();
1974
1975        let err = task.await.unwrap().expect_err("oversized plist must fail");
1976        assert!(err.to_string().contains(&format!(
1977            "plist length {} exceeds max {}",
1978            MAX_PLIST_SIZE + 1,
1979            MAX_PLIST_SIZE
1980        )));
1981    }
1982
1983    #[tokio::test]
1984    async fn handle_message_application_disconnected_clears_cached_state() {
1985        let stream = tokio::io::empty();
1986        let mut client = WebInspectorClient::with_connection_id(stream, "TEST");
1987        client.applications.insert(
1988            "PID:42".into(),
1989            Application {
1990                id: "PID:42".into(),
1991                bundle_identifier: "com.apple.mobilesafari".into(),
1992                pid: 42,
1993                name: "Safari".into(),
1994                availability: AutomationAvailability::Available,
1995                is_active: true,
1996                is_proxy: false,
1997                is_ready: true,
1998                host_application_identifier: None,
1999            },
2000        );
2001        client
2002            .application_pages
2003            .insert("PID:42".into(), IndexMap::new());
2004
2005        let message = plist::Dictionary::from_iter([
2006            (
2007                "__selector".to_string(),
2008                plist::Value::String("_rpc_applicationDisconnected:".into()),
2009            ),
2010            (
2011                "__argument".to_string(),
2012                plist::Value::Dictionary(plist::Dictionary::from_iter([(
2013                    "WIRApplicationIdentifierKey".to_string(),
2014                    plist::Value::String("PID:42".into()),
2015                )])),
2016            ),
2017        ]);
2018
2019        let event = client.handle_message(message).await.unwrap();
2020        assert!(matches!(
2021            event,
2022            WebInspectorEvent::ApplicationDisconnected { ref application_id } if application_id == "PID:42"
2023        ));
2024        assert!(client.applications().is_empty());
2025        assert!(client.application_pages("PID:42").is_none());
2026    }
2027
2028    #[tokio::test]
2029    async fn handle_message_application_listing_merges_existing_page_cache() {
2030        let stream = tokio::io::empty();
2031        let mut client = WebInspectorClient::with_connection_id(stream, "TEST");
2032
2033        client
2034            .handle_message(application_listing_message(
2035                "PID:42",
2036                &[
2037                    (
2038                        "page-1",
2039                        1,
2040                        "WIRTypeWebPage",
2041                        Some("Example"),
2042                        Some("https://example.com"),
2043                    ),
2044                    (
2045                        "page-2",
2046                        2,
2047                        "WIRTypeWebPage",
2048                        Some("Second"),
2049                        Some("https://second.example.com"),
2050                    ),
2051                ],
2052            ))
2053            .await
2054            .unwrap();
2055
2056        let event = client
2057            .handle_message(application_listing_message(
2058                "PID:42",
2059                &[(
2060                    "page-1",
2061                    1,
2062                    "WIRTypeWebPage",
2063                    Some("Updated Example"),
2064                    Some("https://updated.example.com"),
2065                )],
2066            ))
2067            .await
2068            .unwrap();
2069
2070        assert!(matches!(
2071            event,
2072            WebInspectorEvent::Listing { ref application_id, ref pages }
2073                if application_id == "PID:42"
2074                    && pages.len() == 1
2075                    && pages[0].id == 1
2076                    && pages[0].title.as_deref() == Some("Updated Example")
2077        ));
2078
2079        let pages = client
2080            .application_pages("PID:42")
2081            .expect("application pages must exist after listing");
2082        assert_eq!(pages.len(), 2);
2083        assert_eq!(
2084            pages.get(&1).and_then(|page| page.title.as_deref()),
2085            Some("Updated Example")
2086        );
2087        assert_eq!(
2088            pages.get(&1).and_then(|page| page.url.as_deref()),
2089            Some("https://updated.example.com")
2090        );
2091        assert_eq!(
2092            pages.get(&2).and_then(|page| page.title.as_deref()),
2093            Some("Second")
2094        );
2095        assert_eq!(
2096            pages.get(&2).and_then(|page| page.url.as_deref()),
2097            Some("https://second.example.com")
2098        );
2099    }
2100
2101    #[tokio::test]
2102    async fn open_application_pages_returns_snapshot_on_idle_timeout() {
2103        let (client_stream, mut server_stream) = duplex(16 * 1024);
2104        let task = tokio::spawn(async move {
2105            let mut client = WebInspectorClient::with_connection_id(client_stream, "TEST");
2106            client
2107                .open_application_pages(Duration::from_millis(50))
2108                .await
2109                .unwrap()
2110        });
2111
2112        let request = recv_plist(&mut server_stream).await.unwrap();
2113        assert_eq!(
2114            request.get("__selector").and_then(plist::Value::as_string),
2115            Some("_rpc_getConnectedApplications:")
2116        );
2117
2118        server_stream
2119            .write_all(&encode_plist(&plist::Value::Dictionary(
2120                plist::Dictionary::from_iter([
2121                    (
2122                        "__selector".to_string(),
2123                        plist::Value::String("_rpc_reportConnectedApplicationList:".into()),
2124                    ),
2125                    (
2126                        "__argument".to_string(),
2127                        plist::Value::Dictionary(plist::Dictionary::from_iter([(
2128                            "WIRApplicationDictionaryKey".to_string(),
2129                            plist::Value::Dictionary(plist::Dictionary::from_iter([(
2130                                "PID:42".to_string(),
2131                                plist::Value::Dictionary(plist::Dictionary::from_iter([
2132                                    (
2133                                        "WIRApplicationIdentifierKey".to_string(),
2134                                        plist::Value::String("PID:42".into()),
2135                                    ),
2136                                    (
2137                                        "WIRApplicationBundleIdentifierKey".to_string(),
2138                                        plist::Value::String("com.apple.mobilesafari".into()),
2139                                    ),
2140                                    (
2141                                        "WIRApplicationNameKey".to_string(),
2142                                        plist::Value::String("Safari".into()),
2143                                    ),
2144                                    (
2145                                        "WIRAutomationAvailabilityKey".to_string(),
2146                                        plist::Value::String(
2147                                            "WIRAutomationAvailabilityAvailable".into(),
2148                                        ),
2149                                    ),
2150                                    (
2151                                        "WIRIsApplicationActiveKey".to_string(),
2152                                        plist::Value::Boolean(true),
2153                                    ),
2154                                    (
2155                                        "WIRIsApplicationProxyKey".to_string(),
2156                                        plist::Value::Boolean(false),
2157                                    ),
2158                                    (
2159                                        "WIRIsApplicationReadyKey".to_string(),
2160                                        plist::Value::Boolean(true),
2161                                    ),
2162                                ])),
2163                            )])),
2164                        )])),
2165                    ),
2166                ]),
2167            )))
2168            .await
2169            .unwrap();
2170
2171        let listing_request = recv_plist(&mut server_stream).await.unwrap();
2172        assert_eq!(
2173            listing_request
2174                .get("__selector")
2175                .and_then(plist::Value::as_string),
2176            Some("_rpc_forwardGetListing:")
2177        );
2178
2179        server_stream
2180            .write_all(&encode_plist(&plist::Value::Dictionary(
2181                plist::Dictionary::from_iter([
2182                    (
2183                        "__selector".to_string(),
2184                        plist::Value::String("_rpc_applicationSentListing:".into()),
2185                    ),
2186                    (
2187                        "__argument".to_string(),
2188                        plist::Value::Dictionary(plist::Dictionary::from_iter([
2189                            (
2190                                "WIRApplicationIdentifierKey".to_string(),
2191                                plist::Value::String("PID:42".into()),
2192                            ),
2193                            (
2194                                "WIRListingKey".to_string(),
2195                                plist::Value::Dictionary(plist::Dictionary::from_iter([(
2196                                    "page-7".to_string(),
2197                                    plist::Value::Dictionary(plist::Dictionary::from_iter([
2198                                        (
2199                                            "WIRPageIdentifierKey".to_string(),
2200                                            plist::Value::Integer(7.into()),
2201                                        ),
2202                                        (
2203                                            "WIRTypeKey".to_string(),
2204                                            plist::Value::String("WIRTypeWebPage".into()),
2205                                        ),
2206                                    ])),
2207                                )])),
2208                            ),
2209                        ])),
2210                    ),
2211                ]),
2212            )))
2213            .await
2214            .unwrap();
2215
2216        let pages = task.await.unwrap();
2217        assert_eq!(pages.len(), 1);
2218        assert_eq!(pages[0].application.id, "PID:42");
2219        assert_eq!(pages[0].page.id, 7);
2220    }
2221
2222    #[test]
2223    fn inspector_session_observe_message_updates_target_after_provisional_commit() {
2224        let mut session = InspectorSession::with_session_id("PID:42", 1, "TEST-SESSION");
2225        session
2226            .observe_message(&json!({
2227                "method": "Target.targetCreated",
2228                "params": {
2229                    "targetInfo": {
2230                        "targetId": "target-1"
2231                    }
2232                }
2233            }))
2234            .unwrap();
2235        assert_eq!(session.target_id(), Some("target-1"));
2236
2237        session
2238            .observe_message(&json!({
2239                "method": "Target.didCommitProvisionalTarget",
2240                "params": {
2241                    "newTargetId": "target-2"
2242                }
2243            }))
2244            .unwrap();
2245        assert_eq!(session.target_id(), Some("target-2"));
2246    }
2247}