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 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}