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