1use std::sync::Arc;
42use std::time::{Duration, Instant};
43
44use serde::{Deserialize, Serialize};
45
46use crate::app::LOOKUP_POLL_INTERVAL;
47use crate::element::{Element, ElementData, TreeNode};
48use crate::error::{Diagnosis, Error, Result};
49use crate::locator::Locator;
50use crate::provider::Provider;
51
52const DIAG_SURFACE_LIST_LIMIT: usize = 20;
56
57const SHELL_KIND_RAW_KEY: &str = "shell_kind";
73
74#[derive(
81 Debug,
82 Clone,
83 Copy,
84 PartialEq,
85 Eq,
86 Hash,
87 Serialize,
88 Deserialize,
89 strum::EnumString,
90 strum::IntoStaticStr,
91)]
92#[cfg_attr(test, derive(strum::EnumIter))]
97#[strum(serialize_all = "snake_case")]
98#[non_exhaustive]
99pub enum ShellSurfaceKind {
100 MenuBar,
103 StatusItems,
107 Taskbar,
110 Panel,
113 Dock,
115 Desktop,
118 Flyout,
122 Unknown,
127}
128
129impl ShellSurfaceKind {
130 pub const ALL: &'static [ShellSurfaceKind] = &[
143 ShellSurfaceKind::MenuBar,
144 ShellSurfaceKind::StatusItems,
145 ShellSurfaceKind::Taskbar,
146 ShellSurfaceKind::Panel,
147 ShellSurfaceKind::Dock,
148 ShellSurfaceKind::Desktop,
149 ShellSurfaceKind::Flyout,
150 ShellSurfaceKind::Unknown,
151 ];
152
153 pub fn from_snake_case(s: &str) -> Option<Self> {
156 s.parse::<ShellSurfaceKind>().ok()
157 }
158
159 pub fn to_snake_case(self) -> &'static str {
165 self.into()
166 }
167}
168
169impl std::fmt::Display for ShellSurfaceKind {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 write!(f, "{}", self.to_snake_case())
172 }
173}
174
175fn surface_candidates(surfaces: &[ShellSurface]) -> Vec<String> {
180 let total = surfaces.len();
181 let mut out: Vec<String> = surfaces
182 .iter()
183 .take(DIAG_SURFACE_LIST_LIMIT)
184 .map(|s| {
185 let pid = s.pid.map(|p| format!(" (pid={p})")).unwrap_or_default();
186 format!("{} \"{}\"{pid}", s.kind, s.name)
187 })
188 .collect();
189 if total > DIAG_SURFACE_LIST_LIMIT {
190 out.push(format!("… (+{} more)", total - DIAG_SURFACE_LIST_LIMIT));
191 }
192 out
193}
194
195#[allow(
203 clippy::exhaustive_structs,
204 reason = "The private `provider` field already forbids construction and \
205 exhaustive destructuring from other crates, so `#[non_exhaustive]` \
206 would add nothing and growth is not breaking. Recorded rather than \
207 left implicit: the lint stays quiet only because of that field, so \
208 without this the decision reads as never having been made."
209)]
210pub struct ShellSurface {
211 pub kind: ShellSurfaceKind,
213 pub name: String,
218 pub pid: Option<u32>,
225 pub data: ElementData,
227 provider: Arc<dyn Provider>,
228}
229
230impl ShellSurface {
231 pub fn list_with(provider: Arc<dyn Provider>) -> Result<Vec<Self>> {
243 let entries = provider.list_shell_surfaces()?;
244 Ok(entries
245 .into_iter()
246 .map(|(kind, mut data)| {
247 data.raw.insert(
252 SHELL_KIND_RAW_KEY.to_string(),
253 serde_json::Value::String(kind.to_snake_case().to_string()),
254 );
255 let name = data
259 .name
260 .clone()
261 .filter(|n| !n.is_empty())
262 .unwrap_or_else(|| kind.to_snake_case().to_string());
263 let pid = data.pid;
264 Self {
265 kind,
266 name,
267 pid,
268 data,
269 provider: Arc::clone(&provider),
270 }
271 })
272 .collect())
273 }
274
275 pub fn by_kind_with(
298 provider: Arc<dyn Provider>,
299 kind: ShellSurfaceKind,
300 timeout: Duration,
301 ) -> Result<Self> {
302 let selector = format!("shell_surface[kind={kind}]");
306 let start = Instant::now();
307 loop {
308 let mut matched: Vec<Self> = Vec::new();
311 let mut others: Vec<Self> = Vec::new();
312 for surface in Self::list_with(Arc::clone(&provider))? {
313 if surface.kind == kind {
314 matched.push(surface);
315 } else {
316 others.push(surface);
317 }
318 }
319
320 if matched.len() > 1 {
321 return Err(Error::selector_not_matched(selector).diagnose(
325 Diagnosis::new()
326 .condition(format!("exactly one {kind} shell surface"))
327 .last_observed(format!(
328 "{} {kind} surfaces are present; disambiguate with \
329 ShellSurface::list() and pick by pid",
330 matched.len()
331 ))
332 .candidates(surface_candidates(&matched)),
333 ));
334 }
335 if let Some(surface) = matched.pop() {
336 return Ok(surface);
337 }
338 if start.elapsed() >= timeout {
339 return Err(Error::selector_not_matched(selector).diagnose(
340 Diagnosis::new()
341 .condition(format!("a {kind} shell surface"))
342 .last_observed(format!(
343 "no {kind} surface present; {} other shell surface(s) enumerated",
344 others.len()
345 ))
346 .candidates(surface_candidates(&others)),
347 ));
348 }
349 std::thread::sleep(LOOKUP_POLL_INTERVAL);
350 }
351 }
352
353 pub fn locator(&self, selector: &str) -> Locator {
355 Locator::new(
356 Arc::clone(&self.provider),
357 Some(self.data.clone()),
358 selector,
359 )
360 }
361
362 pub fn children(&self) -> Result<Vec<Element>> {
364 let children = self.provider.get_children(Some(&self.data))?;
365 Ok(children
366 .into_iter()
367 .map(|d| Element::new(d, Arc::clone(&self.provider)))
368 .collect())
369 }
370
371 pub fn tree(&self, max_depth: Option<usize>) -> Result<TreeNode> {
377 self.as_element().tree(max_depth)
378 }
379
380 pub fn dump(&self, max_depth: Option<usize>) -> Result<String> {
388 self.as_element().dump(max_depth)
389 }
390
391 pub fn as_element(&self) -> Element {
396 Element::new(self.data.clone(), Arc::clone(&self.provider))
397 }
398}
399
400impl std::fmt::Display for ShellSurface {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 write!(f, "{} \"{}\"", self.kind, self.name)
403 }
404}
405
406impl std::fmt::Debug for ShellSurface {
407 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 f.debug_struct("ShellSurface")
409 .field("kind", &self.kind)
410 .field("name", &self.name)
411 .field("pid", &self.pid)
412 .finish()
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::event_provider::Subscription;
420 use crate::mock::{build_provider, MockProvider, MOCK_SHELL_PID};
421 use crate::role::Role;
422
423 fn surfaces() -> Vec<ShellSurface> {
424 let provider: Arc<dyn Provider> = build_provider();
425 ShellSurface::list_with(provider).expect("the mock must list its shell surfaces")
426 }
427
428 struct DuplicateSurfaceProvider {
432 inner: Arc<MockProvider>,
433 }
434
435 impl DuplicateSurfaceProvider {
436 fn new() -> Self {
437 Self {
438 inner: build_provider(),
439 }
440 }
441 }
442
443 impl Provider for DuplicateSurfaceProvider {
444 fn list_shell_surfaces(&self) -> Result<Vec<(ShellSurfaceKind, ElementData)>> {
445 let mut all = self.inner.list_shell_surfaces()?;
446 let dup = all
447 .iter()
448 .find(|(k, _)| *k == ShellSurfaceKind::Taskbar)
449 .cloned()
450 .expect("the mock fixture must contain a taskbar surface");
451 all.push(dup);
452 Ok(all)
453 }
454 fn activate(&self, e: &ElementData) -> Result<()> {
455 self.inner.activate(e)
456 }
457 fn minimize(&self, e: &ElementData) -> Result<()> {
458 self.inner.minimize(e)
459 }
460 fn maximize(&self, e: &ElementData) -> Result<()> {
461 self.inner.maximize(e)
462 }
463 fn enter_fullscreen(&self, e: &ElementData) -> Result<()> {
464 self.inner.enter_fullscreen(e)
465 }
466 fn restore(&self, e: &ElementData) -> Result<()> {
467 self.inner.restore(e)
468 }
469 fn close(&self, e: &ElementData) -> Result<()> {
470 self.inner.close(e)
471 }
472 fn move_to(&self, e: &ElementData, x: i32, y: i32) -> Result<()> {
473 self.inner.move_to(e, x, y)
474 }
475 fn resize_to(&self, e: &ElementData, w: u32, h: u32) -> Result<()> {
476 self.inner.resize_to(e, w, h)
477 }
478 fn get_children(&self, e: Option<&ElementData>) -> Result<Vec<ElementData>> {
479 self.inner.get_children(e)
480 }
481 fn get_parent(&self, e: &ElementData) -> Result<Option<ElementData>> {
482 self.inner.get_parent(e)
483 }
484 fn list_apps(&self) -> Result<Vec<ElementData>> {
485 self.inner.list_apps()
486 }
487 fn focused_app(&self) -> Result<ElementData> {
488 self.inner.focused_app()
489 }
490 fn press(&self, e: &ElementData) -> Result<()> {
491 self.inner.press(e)
492 }
493 fn focus(&self, e: &ElementData) -> Result<()> {
494 self.inner.focus(e)
495 }
496 fn blur(&self, e: &ElementData) -> Result<()> {
497 self.inner.blur(e)
498 }
499 fn toggle(&self, e: &ElementData) -> Result<()> {
500 self.inner.toggle(e)
501 }
502 fn select(&self, e: &ElementData) -> Result<()> {
503 self.inner.select(e)
504 }
505 fn expand(&self, e: &ElementData) -> Result<()> {
506 self.inner.expand(e)
507 }
508 fn collapse(&self, e: &ElementData) -> Result<()> {
509 self.inner.collapse(e)
510 }
511 fn show_menu(&self, e: &ElementData) -> Result<()> {
512 self.inner.show_menu(e)
513 }
514 fn increment(&self, e: &ElementData) -> Result<()> {
515 self.inner.increment(e)
516 }
517 fn decrement(&self, e: &ElementData) -> Result<()> {
518 self.inner.decrement(e)
519 }
520 fn scroll_into_view(&self, e: &ElementData) -> Result<()> {
521 self.inner.scroll_into_view(e)
522 }
523 fn set_value(&self, e: &ElementData, v: &str) -> Result<()> {
524 self.inner.set_value(e, v)
525 }
526 fn set_numeric_value(&self, e: &ElementData, v: f64) -> Result<()> {
527 self.inner.set_numeric_value(e, v)
528 }
529 fn type_text(&self, e: &ElementData, t: &str) -> Result<()> {
530 self.inner.type_text(e, t)
531 }
532 fn set_text_selection(&self, e: &ElementData, s: u32, end: u32) -> Result<()> {
533 self.inner.set_text_selection(e, s, end)
534 }
535 fn perform_action(&self, e: &ElementData, a: &str) -> Result<()> {
536 self.inner.perform_action(e, a)
537 }
538 fn subscribe(&self, e: &ElementData) -> Result<Subscription> {
539 self.inner.subscribe(e)
540 }
541 }
542
543 struct BrokenShellProvider {
546 inner: Arc<MockProvider>,
547 }
548
549 impl Provider for BrokenShellProvider {
550 fn list_shell_surfaces(&self) -> Result<Vec<(ShellSurfaceKind, ElementData)>> {
551 Err(Error::Platform {
552 code: 55,
553 message: "shell enumeration failed".to_string(),
554 })
555 }
556 fn activate(&self, e: &ElementData) -> Result<()> {
557 self.inner.activate(e)
558 }
559 fn minimize(&self, e: &ElementData) -> Result<()> {
560 self.inner.minimize(e)
561 }
562 fn maximize(&self, e: &ElementData) -> Result<()> {
563 self.inner.maximize(e)
564 }
565 fn enter_fullscreen(&self, e: &ElementData) -> Result<()> {
566 self.inner.enter_fullscreen(e)
567 }
568 fn restore(&self, e: &ElementData) -> Result<()> {
569 self.inner.restore(e)
570 }
571 fn close(&self, e: &ElementData) -> Result<()> {
572 self.inner.close(e)
573 }
574 fn move_to(&self, e: &ElementData, x: i32, y: i32) -> Result<()> {
575 self.inner.move_to(e, x, y)
576 }
577 fn resize_to(&self, e: &ElementData, w: u32, h: u32) -> Result<()> {
578 self.inner.resize_to(e, w, h)
579 }
580 fn get_children(&self, e: Option<&ElementData>) -> Result<Vec<ElementData>> {
581 self.inner.get_children(e)
582 }
583 fn get_parent(&self, e: &ElementData) -> Result<Option<ElementData>> {
584 self.inner.get_parent(e)
585 }
586 fn list_apps(&self) -> Result<Vec<ElementData>> {
587 self.inner.list_apps()
588 }
589 fn focused_app(&self) -> Result<ElementData> {
590 self.inner.focused_app()
591 }
592 fn press(&self, e: &ElementData) -> Result<()> {
593 self.inner.press(e)
594 }
595 fn focus(&self, e: &ElementData) -> Result<()> {
596 self.inner.focus(e)
597 }
598 fn blur(&self, e: &ElementData) -> Result<()> {
599 self.inner.blur(e)
600 }
601 fn toggle(&self, e: &ElementData) -> Result<()> {
602 self.inner.toggle(e)
603 }
604 fn select(&self, e: &ElementData) -> Result<()> {
605 self.inner.select(e)
606 }
607 fn expand(&self, e: &ElementData) -> Result<()> {
608 self.inner.expand(e)
609 }
610 fn collapse(&self, e: &ElementData) -> Result<()> {
611 self.inner.collapse(e)
612 }
613 fn show_menu(&self, e: &ElementData) -> Result<()> {
614 self.inner.show_menu(e)
615 }
616 fn increment(&self, e: &ElementData) -> Result<()> {
617 self.inner.increment(e)
618 }
619 fn decrement(&self, e: &ElementData) -> Result<()> {
620 self.inner.decrement(e)
621 }
622 fn scroll_into_view(&self, e: &ElementData) -> Result<()> {
623 self.inner.scroll_into_view(e)
624 }
625 fn set_value(&self, e: &ElementData, v: &str) -> Result<()> {
626 self.inner.set_value(e, v)
627 }
628 fn set_numeric_value(&self, e: &ElementData, v: f64) -> Result<()> {
629 self.inner.set_numeric_value(e, v)
630 }
631 fn type_text(&self, e: &ElementData, t: &str) -> Result<()> {
632 self.inner.type_text(e, t)
633 }
634 fn set_text_selection(&self, e: &ElementData, s: u32, end: u32) -> Result<()> {
635 self.inner.set_text_selection(e, s, end)
636 }
637 fn perform_action(&self, e: &ElementData, a: &str) -> Result<()> {
638 self.inner.perform_action(e, a)
639 }
640 fn subscribe(&self, e: &ElementData) -> Result<Subscription> {
641 self.inner.subscribe(e)
642 }
643 }
644
645 #[test]
655 fn every_variant_is_in_all() {
656 use strum::IntoEnumIterator;
657
658 let declared: Vec<ShellSurfaceKind> = ShellSurfaceKind::iter().collect();
663 assert_eq!(
664 ShellSurfaceKind::ALL,
665 declared.as_slice(),
666 "ShellSurfaceKind::ALL must list every variant, in declaration order — \
667 every advertised kind list (the CLI's --shell help and error text, MCP's \
668 `shell` enum, both bindings' parse errors) is derived from it"
669 );
670
671 for kind in ShellSurfaceKind::ALL {
675 let named = match kind {
676 ShellSurfaceKind::MenuBar => "menu_bar",
677 ShellSurfaceKind::StatusItems => "status_items",
678 ShellSurfaceKind::Taskbar => "taskbar",
679 ShellSurfaceKind::Panel => "panel",
680 ShellSurfaceKind::Dock => "dock",
681 ShellSurfaceKind::Desktop => "desktop",
682 ShellSurfaceKind::Flyout => "flyout",
683 ShellSurfaceKind::Unknown => "unknown",
684 };
685 assert_eq!(kind.to_snake_case(), named);
686 }
687
688 let unique: std::collections::BTreeSet<&str> = ShellSurfaceKind::ALL
691 .iter()
692 .map(|k| k.to_snake_case())
693 .collect();
694 assert_eq!(unique.len(), ShellSurfaceKind::ALL.len());
695 }
696
697 #[test]
698 fn all_kinds_roundtrip() {
699 for &kind in ShellSurfaceKind::ALL {
702 let s = kind.to_snake_case();
703 assert_eq!(
704 ShellSurfaceKind::from_snake_case(s),
705 Some(kind),
706 "roundtrip failed for {s}"
707 );
708 assert_eq!(format!("{kind}"), s);
709 }
710 assert_eq!(
711 ShellSurfaceKind::StatusItems.to_snake_case(),
712 "status_items"
713 );
714 assert_eq!(ShellSurfaceKind::from_snake_case("not_a_kind"), None);
715 }
716
717 #[test]
718 fn list_with_returns_the_mock_fixture_surfaces() {
719 let surfaces = surfaces();
720 let kinds: Vec<ShellSurfaceKind> = surfaces.iter().map(|s| s.kind).collect();
721 assert_eq!(
722 kinds,
723 vec![ShellSurfaceKind::Taskbar, ShellSurfaceKind::Desktop]
724 );
725 assert_eq!(surfaces[0].name, "Taskbar");
726 assert_eq!(surfaces[0].pid, Some(MOCK_SHELL_PID));
727 }
728
729 #[test]
730 fn list_with_stamps_the_kind_onto_the_root() {
731 let surfaces = surfaces();
734 for surface in &surfaces {
735 assert_eq!(
736 surface.data.raw.get(SHELL_KIND_RAW_KEY),
737 Some(&serde_json::Value::String(
738 surface.kind.to_snake_case().to_string()
739 )),
740 "{surface} must carry its kind in raw"
741 );
742 }
743 }
744
745 #[test]
746 fn list_with_falls_back_to_the_kind_for_an_unnamed_root() {
747 struct UnnamedRootProvider {
750 inner: Arc<MockProvider>,
751 }
752 impl Provider for UnnamedRootProvider {
753 fn list_shell_surfaces(&self) -> Result<Vec<(ShellSurfaceKind, ElementData)>> {
754 let mut all = self.inner.list_shell_surfaces()?;
755 for (_, data) in all.iter_mut() {
756 data.name = None;
757 }
758 Ok(all)
759 }
760 fn activate(&self, e: &ElementData) -> Result<()> {
761 self.inner.activate(e)
762 }
763 fn minimize(&self, e: &ElementData) -> Result<()> {
764 self.inner.minimize(e)
765 }
766 fn maximize(&self, e: &ElementData) -> Result<()> {
767 self.inner.maximize(e)
768 }
769 fn enter_fullscreen(&self, e: &ElementData) -> Result<()> {
770 self.inner.enter_fullscreen(e)
771 }
772 fn restore(&self, e: &ElementData) -> Result<()> {
773 self.inner.restore(e)
774 }
775 fn close(&self, e: &ElementData) -> Result<()> {
776 self.inner.close(e)
777 }
778 fn move_to(&self, e: &ElementData, x: i32, y: i32) -> Result<()> {
779 self.inner.move_to(e, x, y)
780 }
781 fn resize_to(&self, e: &ElementData, w: u32, h: u32) -> Result<()> {
782 self.inner.resize_to(e, w, h)
783 }
784 fn get_children(&self, e: Option<&ElementData>) -> Result<Vec<ElementData>> {
785 self.inner.get_children(e)
786 }
787 fn get_parent(&self, e: &ElementData) -> Result<Option<ElementData>> {
788 self.inner.get_parent(e)
789 }
790 fn list_apps(&self) -> Result<Vec<ElementData>> {
791 self.inner.list_apps()
792 }
793 fn focused_app(&self) -> Result<ElementData> {
794 self.inner.focused_app()
795 }
796 fn press(&self, e: &ElementData) -> Result<()> {
797 self.inner.press(e)
798 }
799 fn focus(&self, e: &ElementData) -> Result<()> {
800 self.inner.focus(e)
801 }
802 fn blur(&self, e: &ElementData) -> Result<()> {
803 self.inner.blur(e)
804 }
805 fn toggle(&self, e: &ElementData) -> Result<()> {
806 self.inner.toggle(e)
807 }
808 fn select(&self, e: &ElementData) -> Result<()> {
809 self.inner.select(e)
810 }
811 fn expand(&self, e: &ElementData) -> Result<()> {
812 self.inner.expand(e)
813 }
814 fn collapse(&self, e: &ElementData) -> Result<()> {
815 self.inner.collapse(e)
816 }
817 fn show_menu(&self, e: &ElementData) -> Result<()> {
818 self.inner.show_menu(e)
819 }
820 fn increment(&self, e: &ElementData) -> Result<()> {
821 self.inner.increment(e)
822 }
823 fn decrement(&self, e: &ElementData) -> Result<()> {
824 self.inner.decrement(e)
825 }
826 fn scroll_into_view(&self, e: &ElementData) -> Result<()> {
827 self.inner.scroll_into_view(e)
828 }
829 fn set_value(&self, e: &ElementData, v: &str) -> Result<()> {
830 self.inner.set_value(e, v)
831 }
832 fn set_numeric_value(&self, e: &ElementData, v: f64) -> Result<()> {
833 self.inner.set_numeric_value(e, v)
834 }
835 fn type_text(&self, e: &ElementData, t: &str) -> Result<()> {
836 self.inner.type_text(e, t)
837 }
838 fn set_text_selection(&self, e: &ElementData, s: u32, end: u32) -> Result<()> {
839 self.inner.set_text_selection(e, s, end)
840 }
841 fn perform_action(&self, e: &ElementData, a: &str) -> Result<()> {
842 self.inner.perform_action(e, a)
843 }
844 fn subscribe(&self, e: &ElementData) -> Result<Subscription> {
845 self.inner.subscribe(e)
846 }
847 }
848
849 let provider: Arc<dyn Provider> = Arc::new(UnnamedRootProvider {
850 inner: build_provider(),
851 });
852 let surfaces = ShellSurface::list_with(provider).expect("list must succeed");
853 assert_eq!(surfaces[0].name, "taskbar");
854 }
855
856 #[test]
857 fn list_with_propagates_enumeration_failures() {
858 let provider: Arc<dyn Provider> = Arc::new(BrokenShellProvider {
861 inner: build_provider(),
862 });
863 let err = ShellSurface::list_with(provider).expect_err("enumeration failure must surface");
864 assert!(matches!(err, Error::Platform { code: 55, .. }));
865 }
866
867 #[test]
868 fn locator_is_rooted_at_the_surface() {
869 let provider: Arc<dyn Provider> = build_provider();
870 let taskbar =
871 ShellSurface::by_kind_with(provider, ShellSurfaceKind::Taskbar, Duration::ZERO)
872 .expect("the mock must vend a taskbar");
873 let el = taskbar
874 .locator("button[name='Show Hidden Icons']")
875 .element()
876 .expect("the taskbar's overflow chevron must be reachable from the surface root");
877 assert_eq!(el.data().role, Role::Button);
878 assert!(matches!(
880 taskbar.locator("button[name='Back']").element(),
881 Err(Error::SelectorNotMatched { .. })
882 ));
883 }
884
885 #[test]
886 fn surface_tree_and_dump_are_rooted_at_the_surface() {
887 let surfaces = surfaces();
888 let node = surfaces[0].tree(None).expect("tree must succeed");
889 assert_eq!(node.name.as_deref(), Some("Taskbar"));
890 assert_eq!(node.children.len(), 2);
891 let dump = surfaces[0].dump(None).expect("dump must succeed");
892 assert!(
893 dump.contains("Show Hidden Icons"),
894 "dump must render the surface subtree: {dump}"
895 );
896 }
897
898 #[test]
899 fn children_and_as_element_expose_the_surface_root() {
900 let surfaces = surfaces();
901 let el = surfaces[0].as_element();
902 assert_eq!(el.data().name.as_deref(), Some("Taskbar"));
903 let children = surfaces[0].children().expect("children must succeed");
904 let names: Vec<Option<&str>> = children.iter().map(|c| c.data().name.as_deref()).collect();
905 assert_eq!(names, vec![Some("Show Hidden Icons"), Some("Volume")]);
906 }
907
908 #[test]
909 fn by_kind_with_resolves_a_unique_surface() {
910 let provider: Arc<dyn Provider> = build_provider();
911 let desktop =
912 ShellSurface::by_kind_with(provider, ShellSurfaceKind::Desktop, Duration::ZERO)
913 .expect("the mock must vend a desktop surface");
914 assert_eq!(desktop.kind, ShellSurfaceKind::Desktop);
915 assert_eq!(desktop.name, "Desktop");
916 }
917
918 #[test]
919 fn by_kind_with_reports_the_surfaces_that_were_present() {
920 let provider: Arc<dyn Provider> = build_provider();
922 let err = ShellSurface::by_kind_with(provider, ShellSurfaceKind::Dock, Duration::ZERO)
923 .expect_err("the mock has no dock surface");
924 let Error::SelectorNotMatched { selector, .. } = &err else {
925 panic!("expected SelectorNotMatched, got: {err:?}");
926 };
927 assert_eq!(selector, "shell_surface[kind=dock]");
928 let diagnosis = err.diagnosis().expect("the terminal failure must diagnose");
929 assert_eq!(
930 diagnosis.candidates,
931 vec![
932 format!("taskbar \"Taskbar\" (pid={MOCK_SHELL_PID})"),
933 format!("desktop \"Desktop\" (pid={MOCK_SHELL_PID})"),
934 ]
935 );
936 }
937
938 #[test]
939 fn by_kind_with_refuses_ambiguity_immediately() {
940 let provider: Arc<dyn Provider> = Arc::new(DuplicateSurfaceProvider::new());
944 let start = Instant::now();
945 let err = ShellSurface::by_kind_with(
946 provider,
947 ShellSurfaceKind::Taskbar,
948 Duration::from_secs(30),
949 )
950 .expect_err("two taskbars must be refused, not first-matched");
951 assert!(matches!(err, Error::SelectorNotMatched { .. }));
952 let diagnosis = err.diagnosis().expect("ambiguity must diagnose");
953 assert_eq!(diagnosis.candidates.len(), 2);
954 assert!(
955 diagnosis
956 .last_observed
957 .as_deref()
958 .is_some_and(|s| s.contains("ShellSurface::list()")),
959 "the diagnosis must say how to disambiguate: {diagnosis:?}"
960 );
961 assert!(
962 start.elapsed() < Duration::from_secs(1),
963 "ambiguity is terminal, not a retry"
964 );
965 }
966
967 #[test]
968 fn by_kind_with_propagates_enumeration_failures_and_fails_fast() {
969 let provider: Arc<dyn Provider> = Arc::new(BrokenShellProvider {
971 inner: build_provider(),
972 });
973 let start = Instant::now();
974 let err = ShellSurface::by_kind_with(
975 provider,
976 ShellSurfaceKind::Taskbar,
977 Duration::from_secs(30),
978 )
979 .expect_err("a real enumeration error must propagate");
980 assert!(matches!(err, Error::Platform { code: 55, .. }));
981 assert!(
982 start.elapsed() < Duration::from_secs(1),
983 "a real enumeration error must fail fast, not wait out the timeout"
984 );
985 }
986}