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