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        .map_err(|error| WebInspectorError::Plist(error.to_string()))?;
1683    stream
1684        .write_all(&(payload.len() as u32).to_be_bytes())
1685        .await?;
1686    stream.write_all(&payload).await?;
1687    stream.flush().await?;
1688    Ok(())
1689}
1690
1691async fn recv_plist<S: AsyncRead + Unpin>(
1692    stream: &mut S,
1693) -> Result<plist::Dictionary, WebInspectorError> {
1694    let mut len_buf = [0u8; 4];
1695    stream.read_exact(&mut len_buf).await?;
1696    let len = u32::from_be_bytes(len_buf) as usize;
1697    if len > MAX_PLIST_SIZE {
1698        return Err(WebInspectorError::Protocol(format!(
1699            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
1700        )));
1701    }
1702    let mut payload = vec![0u8; len];
1703    stream.read_exact(&mut payload).await?;
1704    plist::from_bytes(&payload).map_err(|error| WebInspectorError::Plist(error.to_string()))
1705}
1706
1707fn required_string<'a>(
1708    dict: &'a plist::Dictionary,
1709    key: &str,
1710) -> Result<&'a str, WebInspectorError> {
1711    dict.get(key)
1712        .and_then(plist::Value::as_string)
1713        .ok_or_else(|| WebInspectorError::Protocol(format!("missing string field '{key}'")))
1714}
1715
1716fn optional_string<'a>(dict: &'a plist::Dictionary, key: &str) -> Option<&'a str> {
1717    dict.get(key).and_then(plist::Value::as_string)
1718}
1719
1720fn required_bool(dict: &plist::Dictionary, key: &str) -> Result<bool, WebInspectorError> {
1721    optional_bool(dict, key)
1722        .ok_or_else(|| WebInspectorError::Protocol(format!("missing bool field '{key}'")))
1723}
1724
1725fn optional_bool(dict: &plist::Dictionary, key: &str) -> Option<bool> {
1726    match dict.get(key) {
1727        Some(plist::Value::Boolean(value)) => Some(*value),
1728        Some(plist::Value::Integer(value)) => value
1729            .as_unsigned()
1730            .map(|value| value != 0)
1731            .or_else(|| value.as_signed().map(|value| value != 0)),
1732        _ => None,
1733    }
1734}
1735
1736fn plist_integer_to_u64(value: &plist::Value) -> Option<u64> {
1737    match value {
1738        plist::Value::Integer(value) => value
1739            .as_unsigned()
1740            .or_else(|| value.as_signed().map(|value| value as u64)),
1741        _ => None,
1742    }
1743}
1744
1745fn pid_from_identifier(identifier: &str) -> Result<u64, WebInspectorError> {
1746    identifier
1747        .rsplit(':')
1748        .next()
1749        .ok_or_else(|| {
1750            WebInspectorError::Protocol(format!(
1751                "application identifier '{identifier}' does not contain ':'"
1752            ))
1753        })?
1754        .parse::<u64>()
1755        .map_err(|error| {
1756            WebInspectorError::Protocol(format!(
1757                "failed to parse PID from identifier '{identifier}': {error}"
1758            ))
1759        })
1760}
1761
1762fn extract_json_payload(
1763    dict: &plist::Dictionary,
1764    key: &str,
1765) -> Result<JsonValue, WebInspectorError> {
1766    match dict.get(key) {
1767        Some(plist::Value::Data(payload)) => Ok(serde_json::from_slice(payload)?),
1768        Some(plist::Value::String(payload)) => Ok(serde_json::from_str(payload)?),
1769        Some(other) => Err(WebInspectorError::Protocol(format!(
1770            "{key} expected data/string payload, got {other:?}"
1771        ))),
1772        None => Err(WebInspectorError::Protocol(format!(
1773            "missing JSON payload field '{key}'"
1774        ))),
1775    }
1776}
1777
1778fn remaining_time(deadline: Instant, fallback: Duration) -> Result<Duration, WebInspectorError> {
1779    let now = Instant::now();
1780    if now >= deadline {
1781        return Err(WebInspectorError::Timeout(fallback));
1782    }
1783    Ok(deadline.duration_since(now))
1784}
1785
1786#[cfg(test)]
1787mod tests {
1788    use indexmap::IndexMap;
1789    use tokio::io::{duplex, AsyncWriteExt};
1790
1791    use super::*;
1792
1793    fn encode_plist(value: &plist::Value) -> Vec<u8> {
1794        let mut payload = Vec::new();
1795        plist::to_writer_xml(&mut payload, value).expect("plist serialization");
1796        let mut framed = Vec::with_capacity(payload.len() + 4);
1797        framed.extend_from_slice(&(payload.len() as u32).to_be_bytes());
1798        framed.extend_from_slice(&payload);
1799        framed
1800    }
1801
1802    #[test]
1803    fn open_pages_snapshot_only_includes_pages_for_connected_apps() {
1804        let stream = tokio::io::empty();
1805        let mut client = WebInspectorClient::with_connection_id(stream, "TEST");
1806        client.applications.insert(
1807            "PID:42".into(),
1808            Application {
1809                id: "PID:42".into(),
1810                bundle_identifier: "com.apple.mobilesafari".into(),
1811                pid: 42,
1812                name: "Safari".into(),
1813                availability: AutomationAvailability::Available,
1814                is_active: true,
1815                is_proxy: false,
1816                is_ready: true,
1817                host_application_identifier: None,
1818            },
1819        );
1820        client.application_pages.insert(
1821            "PID:42".into(),
1822            IndexMap::from_iter([(
1823                7,
1824                Page {
1825                    id: 7,
1826                    listing_key: "page-7".into(),
1827                    page_type: WirType::WebPage,
1828                    title: Some("Example".into()),
1829                    url: Some("https://example.com".into()),
1830                    automation_is_paired: None,
1831                    automation_name: None,
1832                    automation_version: None,
1833                    automation_session_id: None,
1834                    automation_connection_id: None,
1835                },
1836            )]),
1837        );
1838        client.application_pages.insert(
1839            "PID:99".into(),
1840            IndexMap::from_iter([(
1841                9,
1842                Page {
1843                    id: 9,
1844                    listing_key: "orphan".into(),
1845                    page_type: WirType::WebPage,
1846                    title: None,
1847                    url: None,
1848                    automation_is_paired: None,
1849                    automation_name: None,
1850                    automation_version: None,
1851                    automation_session_id: None,
1852                    automation_connection_id: None,
1853                },
1854            )]),
1855        );
1856
1857        let snapshot = client.open_pages_snapshot();
1858        assert_eq!(snapshot.len(), 1);
1859        assert_eq!(snapshot[0].application.id, "PID:42");
1860        assert_eq!(snapshot[0].page.id, 7);
1861    }
1862
1863    #[test]
1864    fn extract_json_payload_accepts_string_payload() {
1865        let dict = plist::Dictionary::from_iter([(
1866            "WIRMessageDataKey".to_string(),
1867            plist::Value::String("{\"id\":1}".into()),
1868        )]);
1869
1870        assert_eq!(
1871            extract_json_payload(&dict, "WIRMessageDataKey").unwrap(),
1872            json!({ "id": 1 })
1873        );
1874    }
1875
1876    #[test]
1877    fn pid_from_identifier_rejects_non_numeric_suffix() {
1878        let err = pid_from_identifier("PID:not-a-number")
1879            .expect_err("invalid pid suffix must return an error");
1880        assert!(err
1881            .to_string()
1882            .contains("failed to parse PID from identifier 'PID:not-a-number'"));
1883    }
1884
1885    #[test]
1886    fn normalized_locator_rewrites_common_dom_strategies() {
1887        assert_eq!(
1888            normalized_locator(By::ClassName, "hero"),
1889            ("css selector".into(), ".hero".into())
1890        );
1891        assert_eq!(
1892            normalized_locator(By::Id, "main"),
1893            ("css selector".into(), "[id=\"main\"]".into())
1894        );
1895        assert_eq!(
1896            normalized_locator(By::TagName, "button"),
1897            ("css selector".into(), "button".into())
1898        );
1899    }
1900
1901    #[test]
1902    fn remaining_time_errors_after_deadline() {
1903        let fallback = Duration::from_millis(25);
1904        let err = remaining_time(Instant::now(), fallback)
1905            .expect_err("expired deadlines must become timeout errors");
1906        assert!(matches!(err, WebInspectorError::Timeout(duration) if duration == fallback));
1907    }
1908
1909    #[allow(clippy::type_complexity)]
1910    fn application_listing_message(
1911        application_id: &str,
1912        pages: &[(&str, u64, &str, Option<&str>, Option<&str>)],
1913    ) -> plist::Dictionary {
1914        let listing = pages
1915            .iter()
1916            .map(|(listing_key, page_id, page_type, title, url)| {
1917                let mut page = plist::Dictionary::from_iter([
1918                    (
1919                        "WIRPageIdentifierKey".to_string(),
1920                        plist::Value::Integer((*page_id).into()),
1921                    ),
1922                    (
1923                        "WIRTypeKey".to_string(),
1924                        plist::Value::String((*page_type).to_string()),
1925                    ),
1926                ]);
1927                if let Some(title) = title {
1928                    page.insert(
1929                        "WIRTitleKey".to_string(),
1930                        plist::Value::String((*title).to_string()),
1931                    );
1932                }
1933                if let Some(url) = url {
1934                    page.insert(
1935                        "WIRURLKey".to_string(),
1936                        plist::Value::String((*url).to_string()),
1937                    );
1938                }
1939                ((*listing_key).to_string(), plist::Value::Dictionary(page))
1940            });
1941
1942        plist::Dictionary::from_iter([
1943            (
1944                "__selector".to_string(),
1945                plist::Value::String("_rpc_applicationSentListing:".into()),
1946            ),
1947            (
1948                "__argument".to_string(),
1949                plist::Value::Dictionary(plist::Dictionary::from_iter([
1950                    (
1951                        "WIRApplicationIdentifierKey".to_string(),
1952                        plist::Value::String(application_id.to_string()),
1953                    ),
1954                    (
1955                        "WIRListingKey".to_string(),
1956                        plist::Value::Dictionary(plist::Dictionary::from_iter(listing)),
1957                    ),
1958                ])),
1959            ),
1960        ])
1961    }
1962
1963    #[tokio::test]
1964    async fn recv_plist_rejects_oversized_frames() {
1965        let (client, mut server) = duplex(64);
1966        let task = tokio::spawn(async move {
1967            let mut stream = client;
1968            recv_plist(&mut stream).await
1969        });
1970
1971        server
1972            .write_all(&((MAX_PLIST_SIZE as u32) + 1).to_be_bytes())
1973            .await
1974            .unwrap();
1975
1976        let err = task.await.unwrap().expect_err("oversized plist must fail");
1977        assert!(err.to_string().contains(&format!(
1978            "plist length {} exceeds max {}",
1979            MAX_PLIST_SIZE + 1,
1980            MAX_PLIST_SIZE
1981        )));
1982    }
1983
1984    #[tokio::test]
1985    async fn handle_message_application_disconnected_clears_cached_state() {
1986        let stream = tokio::io::empty();
1987        let mut client = WebInspectorClient::with_connection_id(stream, "TEST");
1988        client.applications.insert(
1989            "PID:42".into(),
1990            Application {
1991                id: "PID:42".into(),
1992                bundle_identifier: "com.apple.mobilesafari".into(),
1993                pid: 42,
1994                name: "Safari".into(),
1995                availability: AutomationAvailability::Available,
1996                is_active: true,
1997                is_proxy: false,
1998                is_ready: true,
1999                host_application_identifier: None,
2000            },
2001        );
2002        client
2003            .application_pages
2004            .insert("PID:42".into(), IndexMap::new());
2005
2006        let message = plist::Dictionary::from_iter([
2007            (
2008                "__selector".to_string(),
2009                plist::Value::String("_rpc_applicationDisconnected:".into()),
2010            ),
2011            (
2012                "__argument".to_string(),
2013                plist::Value::Dictionary(plist::Dictionary::from_iter([(
2014                    "WIRApplicationIdentifierKey".to_string(),
2015                    plist::Value::String("PID:42".into()),
2016                )])),
2017            ),
2018        ]);
2019
2020        let event = client.handle_message(message).await.unwrap();
2021        assert!(matches!(
2022            event,
2023            WebInspectorEvent::ApplicationDisconnected { ref application_id } if application_id == "PID:42"
2024        ));
2025        assert!(client.applications().is_empty());
2026        assert!(client.application_pages("PID:42").is_none());
2027    }
2028
2029    #[tokio::test]
2030    async fn handle_message_application_listing_merges_existing_page_cache() {
2031        let stream = tokio::io::empty();
2032        let mut client = WebInspectorClient::with_connection_id(stream, "TEST");
2033
2034        client
2035            .handle_message(application_listing_message(
2036                "PID:42",
2037                &[
2038                    (
2039                        "page-1",
2040                        1,
2041                        "WIRTypeWebPage",
2042                        Some("Example"),
2043                        Some("https://example.com"),
2044                    ),
2045                    (
2046                        "page-2",
2047                        2,
2048                        "WIRTypeWebPage",
2049                        Some("Second"),
2050                        Some("https://second.example.com"),
2051                    ),
2052                ],
2053            ))
2054            .await
2055            .unwrap();
2056
2057        let event = client
2058            .handle_message(application_listing_message(
2059                "PID:42",
2060                &[(
2061                    "page-1",
2062                    1,
2063                    "WIRTypeWebPage",
2064                    Some("Updated Example"),
2065                    Some("https://updated.example.com"),
2066                )],
2067            ))
2068            .await
2069            .unwrap();
2070
2071        assert!(matches!(
2072            event,
2073            WebInspectorEvent::Listing { ref application_id, ref pages }
2074                if application_id == "PID:42"
2075                    && pages.len() == 1
2076                    && pages[0].id == 1
2077                    && pages[0].title.as_deref() == Some("Updated Example")
2078        ));
2079
2080        let pages = client
2081            .application_pages("PID:42")
2082            .expect("application pages must exist after listing");
2083        assert_eq!(pages.len(), 2);
2084        assert_eq!(
2085            pages.get(&1).and_then(|page| page.title.as_deref()),
2086            Some("Updated Example")
2087        );
2088        assert_eq!(
2089            pages.get(&1).and_then(|page| page.url.as_deref()),
2090            Some("https://updated.example.com")
2091        );
2092        assert_eq!(
2093            pages.get(&2).and_then(|page| page.title.as_deref()),
2094            Some("Second")
2095        );
2096        assert_eq!(
2097            pages.get(&2).and_then(|page| page.url.as_deref()),
2098            Some("https://second.example.com")
2099        );
2100    }
2101
2102    #[tokio::test]
2103    async fn open_application_pages_returns_snapshot_on_idle_timeout() {
2104        let (client_stream, mut server_stream) = duplex(16 * 1024);
2105        let task = tokio::spawn(async move {
2106            let mut client = WebInspectorClient::with_connection_id(client_stream, "TEST");
2107            client
2108                .open_application_pages(Duration::from_millis(50))
2109                .await
2110                .unwrap()
2111        });
2112
2113        let request = recv_plist(&mut server_stream).await.unwrap();
2114        assert_eq!(
2115            request.get("__selector").and_then(plist::Value::as_string),
2116            Some("_rpc_getConnectedApplications:")
2117        );
2118
2119        server_stream
2120            .write_all(&encode_plist(&plist::Value::Dictionary(
2121                plist::Dictionary::from_iter([
2122                    (
2123                        "__selector".to_string(),
2124                        plist::Value::String("_rpc_reportConnectedApplicationList:".into()),
2125                    ),
2126                    (
2127                        "__argument".to_string(),
2128                        plist::Value::Dictionary(plist::Dictionary::from_iter([(
2129                            "WIRApplicationDictionaryKey".to_string(),
2130                            plist::Value::Dictionary(plist::Dictionary::from_iter([(
2131                                "PID:42".to_string(),
2132                                plist::Value::Dictionary(plist::Dictionary::from_iter([
2133                                    (
2134                                        "WIRApplicationIdentifierKey".to_string(),
2135                                        plist::Value::String("PID:42".into()),
2136                                    ),
2137                                    (
2138                                        "WIRApplicationBundleIdentifierKey".to_string(),
2139                                        plist::Value::String("com.apple.mobilesafari".into()),
2140                                    ),
2141                                    (
2142                                        "WIRApplicationNameKey".to_string(),
2143                                        plist::Value::String("Safari".into()),
2144                                    ),
2145                                    (
2146                                        "WIRAutomationAvailabilityKey".to_string(),
2147                                        plist::Value::String(
2148                                            "WIRAutomationAvailabilityAvailable".into(),
2149                                        ),
2150                                    ),
2151                                    (
2152                                        "WIRIsApplicationActiveKey".to_string(),
2153                                        plist::Value::Boolean(true),
2154                                    ),
2155                                    (
2156                                        "WIRIsApplicationProxyKey".to_string(),
2157                                        plist::Value::Boolean(false),
2158                                    ),
2159                                    (
2160                                        "WIRIsApplicationReadyKey".to_string(),
2161                                        plist::Value::Boolean(true),
2162                                    ),
2163                                ])),
2164                            )])),
2165                        )])),
2166                    ),
2167                ]),
2168            )))
2169            .await
2170            .unwrap();
2171
2172        let listing_request = recv_plist(&mut server_stream).await.unwrap();
2173        assert_eq!(
2174            listing_request
2175                .get("__selector")
2176                .and_then(plist::Value::as_string),
2177            Some("_rpc_forwardGetListing:")
2178        );
2179
2180        server_stream
2181            .write_all(&encode_plist(&plist::Value::Dictionary(
2182                plist::Dictionary::from_iter([
2183                    (
2184                        "__selector".to_string(),
2185                        plist::Value::String("_rpc_applicationSentListing:".into()),
2186                    ),
2187                    (
2188                        "__argument".to_string(),
2189                        plist::Value::Dictionary(plist::Dictionary::from_iter([
2190                            (
2191                                "WIRApplicationIdentifierKey".to_string(),
2192                                plist::Value::String("PID:42".into()),
2193                            ),
2194                            (
2195                                "WIRListingKey".to_string(),
2196                                plist::Value::Dictionary(plist::Dictionary::from_iter([(
2197                                    "page-7".to_string(),
2198                                    plist::Value::Dictionary(plist::Dictionary::from_iter([
2199                                        (
2200                                            "WIRPageIdentifierKey".to_string(),
2201                                            plist::Value::Integer(7.into()),
2202                                        ),
2203                                        (
2204                                            "WIRTypeKey".to_string(),
2205                                            plist::Value::String("WIRTypeWebPage".into()),
2206                                        ),
2207                                    ])),
2208                                )])),
2209                            ),
2210                        ])),
2211                    ),
2212                ]),
2213            )))
2214            .await
2215            .unwrap();
2216
2217        let pages = task.await.unwrap();
2218        assert_eq!(pages.len(), 1);
2219        assert_eq!(pages[0].application.id, "PID:42");
2220        assert_eq!(pages[0].page.id, 7);
2221    }
2222
2223    #[test]
2224    fn inspector_session_observe_message_updates_target_after_provisional_commit() {
2225        let mut session = InspectorSession::with_session_id("PID:42", 1, "TEST-SESSION");
2226        session
2227            .observe_message(&json!({
2228                "method": "Target.targetCreated",
2229                "params": {
2230                    "targetInfo": {
2231                        "targetId": "target-1"
2232                    }
2233                }
2234            }))
2235            .unwrap();
2236        assert_eq!(session.target_id(), Some("target-1"));
2237
2238        session
2239            .observe_message(&json!({
2240                "method": "Target.didCommitProvisionalTarget",
2241                "params": {
2242                    "newTargetId": "target-2"
2243                }
2244            }))
2245            .unwrap();
2246        assert_eq!(session.target_id(), Some("target-2"));
2247    }
2248}