Skip to main content

xa11y_core/
shell.rs

1//! OS shell surfaces — the taskbar, desktop panels, docks, menu bars, status
2//! items, the desktop itself, and transient shell flyouts.
3//!
4//! Shell UI is already an ordinary part of the platform accessibility tree;
5//! what is missing is a way to *find* it, because the per-platform root
6//! filters behind [`Provider::list_apps`] drop it. This module adds exactly
7//! that: a discovery primitive
8//! ([`Provider::list_shell_surfaces`](crate::provider::Provider::list_shell_surfaces))
9//! and a handle type ([`ShellSurface`]) shaped like [`App`](crate::App).
10//! Selectors, locators, auto-wait, actions, `tree` / `dump`, [`Element`] and
11//! [`Diagnosis`] are reused unchanged — every surface wraps a **real**
12//! platform element, never a synthetic node xa11y invented.
13//!
14//! Enumerating surfaces and reading their trees never opens, closes, focuses,
15//! or presses anything. Where content only exists after a press (the Windows
16//! tray overflow, macOS Control Center), the caller performs that press on a
17//! real, advertised element and then re-enumerates:
18//!
19//! ```no_run
20//! # use std::sync::Arc;
21//! # use std::time::Duration;
22//! # use xa11y_core::{Provider, ShellSurface, ShellSurfaceKind};
23//! # fn demo(provider: Arc<dyn Provider>) -> xa11y_core::Result<()> {
24//! let taskbar = ShellSurface::by_kind_with(
25//!     Arc::clone(&provider),
26//!     ShellSurfaceKind::Taskbar,
27//!     Duration::ZERO,
28//! )?;
29//! taskbar.locator("button[name='Show Hidden Icons']").press()?;
30//! // The overflow window materialises asynchronously — wait for it.
31//! let flyout = ShellSurface::by_kind_with(
32//!     provider,
33//!     ShellSurfaceKind::Flyout,
34//!     Duration::from_secs(3),
35//! )?;
36//! flyout.locator("button[name*='Sync']").press()?;
37//! # Ok(())
38//! # }
39//! ```
40
41use 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
52/// Maximum number of shell surfaces listed in a lookup-failure diagnosis.
53/// Bounded per tenet 6 — diagnostics must not grow with an unbounded
54/// environment. Mirrors `DIAG_APP_LIST_LIMIT` in `app.rs`.
55const DIAG_SURFACE_LIST_LIMIT: usize = 20;
56
57/// Raw-attribute key carrying the surface kind on a surface root.
58///
59/// [`ShellSurface::list_with`] stamps it in one place — no backend can drift
60/// on the spelling — and it lands on the **surface root only**, nowhere else
61/// in the tree. A consumer reads it back through
62/// [`as_element`](ShellSurface::as_element):
63/// `surface.as_element().raw["shell_kind"]`.
64///
65/// It is deliberately *not* a way to find a surface. A rooted [`Locator`]
66/// emits only descendants of its root, so `[shell_kind='taskbar']` never
67/// matches the root that carries the stamp, and [`TreeNode`] carries no `raw`
68/// map, so `tree` / `dump` output does not show it either. The kind travels in
69/// [`ShellSurface::kind`] and in the
70/// [`list_shell_surfaces`](crate::provider::Provider::list_shell_surfaces)
71/// signature; the stamp is the same fact, readable from a bare [`Element`].
72const SHELL_KIND_RAW_KEY: &str = "shell_kind";
73
74/// What kind of OS shell surface a [`ShellSurface`] is.
75///
76/// `#[non_exhaustive]`: backends map platform → kind, never the reverse (the
77/// same direction as [`Role`](crate::Role)), and the set grows as more shell
78/// UI is classified — Start menu, jump lists, secondary taskbars, widget
79/// boards.
80#[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// Test-only: `EnumIter` is what lets `every_variant_is_in_all` compare
93// [`ALL`](Self::ALL) against the real variant set instead of against a
94// hand-counted length. Gated so the generated iterator type stays out of the
95// public API (and out of the bindings-parity surface).
96#[cfg_attr(test, derive(strum::EnumIter))]
97#[strum(serialize_all = "snake_case")]
98#[non_exhaustive]
99pub enum ShellSurfaceKind {
100    /// The system menu bar: the frontmost application's menus, Apple menu
101    /// included. macOS only — Windows and Linux have no equivalent object.
102    MenuBar,
103    /// One process's status items (`AXExtrasMenuBar`). One surface per owning
104    /// process; macOS only. The Windows tray has no per-app hosting, so tray
105    /// icons live inside the [`Taskbar`](Self::Taskbar) surface instead.
106    StatusItems,
107    /// The Windows taskbar (`Shell_TrayWnd`), including the task band, the
108    /// visible tray row, and the overflow chevron.
109    Taskbar,
110    /// A Linux desktop panel or dock: an AT-SPI frame carrying the
111    /// `window-type:dock` attribute. One surface per frame.
112    Panel,
113    /// The macOS Dock.
114    Dock,
115    /// The desktop icon surface: `Progman`'s list view on Windows, Finder's
116    /// desktop scroll area on macOS.
117    Desktop,
118    /// A transient shell window that exists only while open: the tray
119    /// overflow flyout, Quick Settings, Notification Center, a shell
120    /// context-menu popup, an opened Control Center panel.
121    Flyout,
122    /// A shell-owned window the backend could not classify. The documented
123    /// fallback, like [`Role::Unknown`](crate::Role::Unknown) — present so a
124    /// new OS surface degrades to "reachable but untagged" rather than
125    /// invisible.
126    Unknown,
127}
128
129impl ShellSurfaceKind {
130    /// Every kind, in the order the CLI, MCP and both bindings advertise them.
131    ///
132    /// The single source for every list of kind spellings outside this crate:
133    /// the CLI's `--shell` help and error message, MCP's `shell` enum, and
134    /// each binding's parse-failure message all derive from
135    /// `ALL` + [`to_snake_case`](Self::to_snake_case) rather than writing the
136    /// strings out again. `ShellSurfaceKind` is `#[non_exhaustive]`, so a
137    /// downstream `match` cannot be the thing that fails when a variant is
138    /// added; `every_variant_is_in_all` in this module's tests is. It matches
139    /// exhaustively (legal in the defining crate, so a new variant is a
140    /// compile error there) and compares this list against strum's derived
141    /// iterator, so `ALL` cannot quietly fall one kind behind the enum.
142    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    /// Parse a snake_case kind name into a `ShellSurfaceKind` variant.
154    /// Returns `None` if the name doesn't match any known kind.
155    pub fn from_snake_case(s: &str) -> Option<Self> {
156        s.parse::<ShellSurfaceKind>().ok()
157    }
158
159    /// Convert a `ShellSurfaceKind` to its snake_case string representation.
160    ///
161    /// This is the spelling the kind carries across every surface: the
162    /// bindings, the CLI's `--shell` flag, MCP's `shell` parameter, and the
163    /// `shell_kind` raw attribute on the surface root.
164    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
175/// Bounded `kind "name" (pid=N)` rendering of the surfaces that *were* found —
176/// the candidate list a consumer would otherwise produce by hand-logging
177/// `ShellSurface::list()` around the failure. Mirrors `running_apps_diagnosis`
178/// in `app.rs`.
179fn 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/// A shell surface: one OS-owned top-level accessibility root, tagged with
196/// what it is. The entry point for shell queries, as [`App`](crate::App) is
197/// for application queries.
198///
199/// `ShellSurface` is **not** an [`Element`] — it represents the surface as a
200/// whole and provides a [`locator`](ShellSurface::locator) to search its
201/// accessibility tree.
202#[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    /// What this surface is.
212    pub kind: ShellSurfaceKind,
213    /// Human-readable name: the owning app for per-app surfaces ("Safari"
214    /// menu bar, "Arq" status items), the platform's own name otherwise
215    /// ("Taskbar", "Dock"). Falls back to the kind's snake_case spelling when
216    /// the platform vends no name for the root.
217    pub name: String,
218    /// Owning process where the platform reports one honestly. On macOS this
219    /// is always the true owner. On Windows it is the process that hosts the
220    /// surface: explorer.exe / ShellHost.exe for shell chrome, or the owning
221    /// application for a native popup menu. UIA carries no per-icon owner, so
222    /// taskbar and tray-icon elements still report their shell host, never a
223    /// guessed application. On Linux it is the panel process.
224    pub pid: Option<u32>,
225    /// The surface's root element data.
226    pub data: ElementData,
227    provider: Arc<dyn Provider>,
228}
229
230impl ShellSurface {
231    /// List the OS shell surfaces currently on screen, using an explicit
232    /// provider.
233    ///
234    /// Prefer `ShellSurface::list` from the `xa11y` crate which uses the
235    /// global singleton provider. Use this variant when you need to supply a
236    /// specific provider (e.g. a mock in unit tests).
237    ///
238    /// The listing is live: [`Flyout`](ShellSurfaceKind::Flyout) surfaces
239    /// appear only while they are open, and enumerating never opens, closes,
240    /// or presses anything. A platform with no surface of a given kind simply
241    /// returns none — that is honest scope, not a failure.
242    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                // The kind is stamped onto the root's raw map HERE — the one
248                // place — rather than in each backend, so no backend can drift
249                // on the spelling. It lands on the surface root only; see
250                // `SHELL_KIND_RAW_KEY` for what that does and does not buy.
251                data.raw.insert(
252                    SHELL_KIND_RAW_KEY.to_string(),
253                    serde_json::Value::String(kind.to_snake_case().to_string()),
254                );
255                // Platform roots are not always named (a `Progman` list view
256                // has no name of its own); the kind is the honest fallback,
257                // and it is what the caller asked for by name anyway.
258                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    /// Wait for **exactly one** surface of `kind`, using an explicit provider.
276    ///
277    /// Prefer `ShellSurface::by_kind` from the `xa11y` crate which uses the
278    /// global singleton provider.
279    ///
280    /// Polls [`list_with`](Self::list_with) until a single surface of `kind`
281    /// exists or `timeout` elapses; `Duration::ZERO` performs exactly one
282    /// attempt (no waiting). An enumeration failure short-circuits — "the
283    /// shell cannot be listed" is not "the surface isn't up yet". The wait is
284    /// what makes the flyout workflow a one-liner: press the tray chevron,
285    /// then wait for the overflow window to materialise.
286    ///
287    /// # Errors
288    ///
289    /// Returns [`Error::SelectorNotMatched`] both when no surface of `kind`
290    /// is present and when **several** are — a second `Panel` frame, status
291    /// items from several processes, a leftover `Flyout`. Ambiguity is
292    /// refused rather than first-matched: for a surface whose consumers are
293    /// mostly agents, silently acting on one of several indistinguishable
294    /// candidates is the failure mode worth ruling out. The refusal is
295    /// terminal, not a retry — the diagnosis lists the candidates so the
296    /// caller can disambiguate with [`list_with`](Self::list_with) and a pid.
297    pub fn by_kind_with(
298        provider: Arc<dyn Provider>,
299        kind: ShellSurfaceKind,
300        timeout: Duration,
301    ) -> Result<Self> {
302        // The selector-shaped string the failures report. Shell surfaces are
303        // not addressable by selector, but the lookup's error reads like every
304        // other not-found in the API.
305        let selector = format!("shell_surface[kind={kind}]");
306        let start = Instant::now();
307        loop {
308            // Split rather than filter: the surfaces of other kinds are the
309            // candidate list a "no such surface" failure owes the caller.
310            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                // Terminal: waiting cannot make an ambiguous shell less
322                // ambiguous, and returning the first would hide which of the
323                // candidates the caller actually got.
324                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    /// Create a [`Locator`] to search this surface's accessibility tree.
354    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    /// Get direct children of the surface root.
363    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    /// Capture the surface's accessibility tree as a recursive snapshot,
372    /// rooted at the surface element.
373    ///
374    /// Equivalent to `self.as_element().tree(max_depth)`. See
375    /// [`Element::tree`] for `max_depth` semantics.
376    pub fn tree(&self, max_depth: Option<usize>) -> Result<TreeNode> {
377        self.as_element().tree(max_depth)
378    }
379
380    /// Render the surface's accessibility tree as an indented string, rooted
381    /// at the surface element.
382    ///
383    /// The primary inspection helper for figuring out the role/name of every
384    /// element in a shell surface before writing selectors. Equivalent to
385    /// `self.as_element().dump(max_depth)`. See [`Element::dump`] for the
386    /// output format.
387    pub fn dump(&self, max_depth: Option<usize>) -> Result<String> {
388        self.as_element().dump(max_depth)
389    }
390
391    /// Get an [`Element`] handle for the surface root.
392    ///
393    /// Useful when you want to use Element-level methods (e.g. `tree`,
394    /// `dump`, `children`) without going through a locator.
395    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    /// Wraps the standard mock provider but reports the taskbar surface twice,
429    /// modelling the ambiguous shell (two `Panel` frames, status items from
430    /// several processes, a leftover flyout). Everything else delegates.
431    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    /// Provider whose shell enumeration fails outright — the "accessibility is
544    /// broken" case, which must never be mistaken for "no surface yet".
545    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    /// `ShellSurfaceKind::ALL` must list every variant.
646    ///
647    /// This is the guard `#[non_exhaustive]` took away: outside this crate a
648    /// `match` needs a `_` arm, so nothing downstream fails to compile when a
649    /// variant is added. Inside the defining crate an exhaustive `match` is
650    /// still legal — so adding a variant without adding it to `ALL` is a
651    /// **compile error here**, and every derived list (the CLI's `--shell`
652    /// help and error text, MCP's `shell` enum, both bindings' parse errors)
653    /// picks the new kind up for free.
654    #[test]
655    fn every_variant_is_in_all() {
656        use strum::IntoEnumIterator;
657
658        // The derived iterator is the second, independent enumeration of the
659        // variants: `ALL` cannot silently fall behind it, because adding a
660        // variant changes what `iter()` yields whether or not anyone edits
661        // this file.
662        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        // And the exhaustive `match` that makes a new variant a compile error
672        // *here*, so the author is sent to this test rather than shipping a
673        // kind nothing advertises. No `_` arm, on purpose.
674        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        // No duplicate spellings: `ALL` is what every advertised list, and
689        // every JSON Schema `enum`, is built from.
690        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        // Every kind must parse back from its own snake_case representation —
700        // that string is what crosses the bindings, the CLI and MCP.
701        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        // The stamp lands on the surface root, and `as_element().raw` is
732        // where a consumer reads it back — the only place it is visible.
733        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        // A platform root without a name of its own (Progman's list view)
748        // still needs a name; the kind is the honest one.
749        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        // Tenet 1: a broken shell enumeration surfaces, it does not become an
859        // empty list.
860        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        // The app tree is NOT in scope: the surface root is its own subtree.
879        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        // Tenet 6: a "no such surface" failure names what *was* there.
921        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        // Several surfaces of one kind: refuse with the candidate list rather
941        // than acting on an arbitrary one — and refuse *now*, since waiting
942        // cannot make the shell less ambiguous.
943        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        // A broken enumeration is not "not yet" — it short-circuits the poll.
970        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}