1//! Visual test platform that combines real rendering (macOs-only for now) with controllable TestDispatcher.
2//!
3//! This platform is used for visual tests that need:
4//! - Real rendering (e.g. Metal/compositor) for accurate screenshots
5//! - Deterministic task scheduling via TestDispatcher
6//! - Controllable time via `advance_clock`
7
8#[cfg(feature = "screen-capture")]
9use crate::ScreenCaptureSource;
10use crate::{
11 AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, Keymap,
12 Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
13 PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
14 TestDispatcher, WindowAppearance, WindowParams,
15};
16use anyhow::Result;
17use futures::channel::oneshot;
18use parking_lot::Mutex;
19
20use std::{
21 path::{Path, PathBuf},
22 rc::Rc,
23 sync::Arc,
24};
25
26/// A platform that combines real Mac rendering with controllable TestDispatcher.
27///
28/// This allows visual tests to:
29/// - Render real UI via Metal for accurate screenshots
30/// - Control task scheduling deterministically via TestDispatcher
31/// - Advance simulated time for testing time-based behaviors (tooltips, animations, etc.)
32pub struct VisualTestPlatform {
33 dispatcher: TestDispatcher,
34 background_executor: BackgroundExecutor,
35 foreground_executor: ForegroundExecutor,
36 platform: Rc<dyn Platform>,
37 clipboard: Mutex<Option<ClipboardItem>>,
38 find_pasteboard: Mutex<Option<ClipboardItem>>,
39}
40
41impl VisualTestPlatform {
42 /// Creates a new VisualTestPlatform with the given random seed.
43 ///
44 /// The seed is used for deterministic random number generation in the TestDispatcher.
45 pub fn new(platform: Rc<dyn Platform>, seed: u64) -> Self {
46 let dispatcher = TestDispatcher::new(seed);
47 let arc_dispatcher = Arc::new(dispatcher.clone());
48
49 let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
50 let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
51
52 Self {
53 dispatcher,
54 background_executor,
55 foreground_executor,
56 platform,
57 clipboard: Mutex::new(None),
58 find_pasteboard: Mutex::new(None),
59 }
60 }
61
62 /// Returns a reference to the TestDispatcher for controlling task scheduling and time.
63 pub fn dispatcher(&self) -> &TestDispatcher {
64 &self.dispatcher
65 }
66}
67
68impl Platform for VisualTestPlatform {
69 fn background_executor(&self) -> BackgroundExecutor {
70 self.background_executor.clone()
71 }
72
73 fn foreground_executor(&self) -> ForegroundExecutor {
74 self.foreground_executor.clone()
75 }
76
77 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
78 self.platform.text_system()
79 }
80
81 fn run(&self, _on_finish_launching: Box<dyn 'static + FnOnce()>) {
82 panic!("VisualTestPlatform::run should not be called in tests")
83 }
84
85 fn quit(&self) {}
86
87 fn restart(&self, _binary_path: Option<PathBuf>) {}
88
89 fn activate(&self, _ignoring_other_apps: bool) {}
90
91 fn hide(&self) {}
92
93 fn hide_other_apps(&self) {}
94
95 fn unhide_other_apps(&self) {}
96
97 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
98 self.platform.displays()
99 }
100
101 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
102 self.platform.primary_display()
103 }
104
105 fn active_window(&self) -> Option<AnyWindowHandle> {
106 self.platform.active_window()
107 }
108
109 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
110 self.platform.window_stack()
111 }
112
113 fn is_screen_capture_supported(&self) -> bool {
114 self.platform.is_screen_capture_supported()
115 }
116
117 fn screen_capture_sources(
118 &self,
119 ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
120 self.platform.screen_capture_sources()
121 }
122
123 fn open_window(
124 &self,
125 handle: AnyWindowHandle,
126 options: WindowParams,
127 ) -> Result<Box<dyn PlatformWindow>> {
128 self.platform.open_window(handle, options)
129 }
130
131 fn window_appearance(&self) -> WindowAppearance {
132 self.platform.window_appearance()
133 }
134
135 fn open_url(&self, url: &str) {
136 self.platform.open_url(url)
137 }
138
139 fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {}
140
141 fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
142 Task::ready(Ok(()))
143 }
144
145 fn prompt_for_paths(
146 &self,
147 _options: PathPromptOptions,
148 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
149 let (tx, rx) = oneshot::channel();
150 tx.send(Ok(None)).ok();
151 rx
152 }
153
154 fn prompt_for_new_path(
155 &self,
156 _directory: &Path,
157 _suggested_name: Option<&str>,
158 ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
159 let (tx, rx) = oneshot::channel();
160 tx.send(Ok(None)).ok();
161 rx
162 }
163
164 fn can_select_mixed_files_and_dirs(&self) -> bool {
165 true
166 }
167
168 fn reveal_path(&self, path: &Path) {
169 self.platform.reveal_path(path)
170 }
171
172 fn open_with_system(&self, path: &Path) {
173 self.platform.open_with_system(path)
174 }
175
176 fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
177
178 fn on_reopen(&self, _callback: Box<dyn FnMut()>) {}
179
180 fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
181
182 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
183 None
184 }
185
186 fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
187
188 fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
189
190 fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
191
192 fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
193
194 fn app_path(&self) -> Result<PathBuf> {
195 self.platform.app_path()
196 }
197
198 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
199 self.platform.path_for_auxiliary_executable(name)
200 }
201
202 fn set_cursor_style(&self, style: CursorStyle) {
203 self.platform.set_cursor_style(style)
204 }
205
206 fn should_auto_hide_scrollbars(&self) -> bool {
207 self.platform.should_auto_hide_scrollbars()
208 }
209
210 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
211 self.clipboard.lock().clone()
212 }
213
214 fn write_to_clipboard(&self, item: ClipboardItem) {
215 *self.clipboard.lock() = Some(item);
216 }
217
218 #[cfg(target_os = "macos")]
219 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
220 self.find_pasteboard.lock().clone()
221 }
222
223 #[cfg(target_os = "macos")]
224 fn write_to_find_pasteboard(&self, item: ClipboardItem) {
225 *self.find_pasteboard.lock() = Some(item);
226 }
227
228 fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
229 Task::ready(Ok(()))
230 }
231
232 fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
233 Task::ready(Ok(None))
234 }
235
236 fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
237 Task::ready(Ok(()))
238 }
239
240 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
241 self.platform.keyboard_layout()
242 }
243
244 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
245 self.platform.keyboard_mapper()
246 }
247
248 fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {}
249
250 fn thermal_state(&self) -> super::ThermalState {
251 super::ThermalState::Nominal
252 }
253
254 fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
255}