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