teksilo_automation/recording_ops.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! A [`WindowOps`] implementation for the headless server.
5//!
6//! [`NoopWindowOps`](teksilo_core::NoopWindowOps) *panics* on `open_window`.
7//! That is the right behaviour for a standalone test tree, but it is wrong
8//! for automation: an AT action (a menu item, a "New window" button) can
9//! legitimately call `open_window`, and crashing the whole server on it
10//! would be a terrible failure mode. [`RecordingWindowOps`] instead
11//! **records a summary of the requested window and returns a synthetic
12//! id**, so the action completes and the harness can observe that a window
13//! was requested via [`RecordingWindowOps::opened`].
14
15use teksilo_core::{TeksiloWindowId, WindowConfig, WindowOps, WindowState};
16
17/// A summary of an `open_window` request. `WindowConfig` is not `Clone`
18/// (it holds `FnOnce` root builders), so the recorder keeps only its
19/// inspectable scalar fields.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct RecordedWindow {
22 /// The requested window title.
23 pub title: String,
24 /// The requested stable `string_id`, if any.
25 pub string_id: Option<String>,
26 /// The requested `(width, height)`.
27 pub size: (u32, u32),
28 /// The synthetic id this recorder returned for the request.
29 pub assigned_id: TeksiloWindowId,
30}
31
32/// A non-panicking `WindowOps` for the headless automation server: it
33/// records `open_window` requests and hands back synthetic ids; every
34/// other method no-ops (matching `NoopWindowOps`).
35#[derive(Debug, Default)]
36pub struct RecordingWindowOps {
37 next_id: u64,
38 /// Every `open_window` request seen, in order.
39 pub opened: Vec<RecordedWindow>,
40 /// Window ids passed to `focus_window`, in order.
41 pub focused: Vec<TeksiloWindowId>,
42 /// Window ids passed to `close_window_by_id`, in order.
43 pub closed: Vec<TeksiloWindowId>,
44}
45
46impl RecordingWindowOps {
47 pub fn new() -> Self {
48 Self::default()
49 }
50}
51
52impl WindowOps for RecordingWindowOps {
53 fn open_window(&mut self, config: WindowConfig) -> TeksiloWindowId {
54 // Synthetic ids start at a high base so they never collide with a
55 // real window manager's allocator in a mixed test.
56 let id = TeksiloWindowId::new(0x1000_0000 + self.next_id);
57 self.next_id += 1;
58 self.opened.push(RecordedWindow {
59 title: config.title.clone(),
60 string_id: config.string_id.clone(),
61 size: config.size,
62 assigned_id: id,
63 });
64 id
65 }
66
67 fn find_window(&self, string_id: &str) -> Option<TeksiloWindowId> {
68 self.opened
69 .iter()
70 .find(|w| w.string_id.as_deref() == Some(string_id))
71 .map(|w| w.assigned_id)
72 }
73
74 fn window_state(&self, _id: TeksiloWindowId) -> Option<WindowState> {
75 None
76 }
77
78 fn windows(&self) -> Vec<WindowState> {
79 Vec::new()
80 }
81
82 fn focus_window(&mut self, id: TeksiloWindowId) {
83 self.focused.push(id);
84 }
85
86 fn close_window_by_id(&mut self, id: TeksiloWindowId) {
87 self.closed.push(id);
88 }
89}