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 MacPlatform, 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 mac_platform: MacPlatform,
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(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 let mac_platform = MacPlatform::new(false);
53
54 Self {
55 dispatcher,
56 background_executor,
57 foreground_executor,
58 mac_platform,
59 clipboard: Mutex::new(None),
60 find_pasteboard: Mutex::new(None),
61 }
62 }
63
64 /// Returns a reference to the TestDispatcher for controlling task scheduling and time.
65 pub fn dispatcher(&self) -> &TestDispatcher {
66 &self.dispatcher
67 }
68}
69
70impl Platform for VisualTestPlatform {
71 fn background_executor(&self) -> BackgroundExecutor {
72 self.background_executor.clone()
73 }
74
75 fn foreground_executor(&self) -> ForegroundExecutor {
76 self.foreground_executor.clone()
77 }
78
79 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
80 self.mac_platform.text_system()
81 }
82
83 fn run(&self, _on_finish_launching: Box<dyn 'static + FnOnce()>) {
84 panic!("VisualTestPlatform::run should not be called in tests")
85 }
86
87 fn quit(&self) {}
88
89 fn restart(&self, _binary_path: Option<PathBuf>) {}
90
91 fn activate(&self, _ignoring_other_apps: bool) {}
92
93 fn hide(&self) {}
94
95 fn hide_other_apps(&self) {}
96
97 fn unhide_other_apps(&self) {}
98
99 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
100 self.mac_platform.displays()
101 }
102
103 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
104 self.mac_platform.primary_display()
105 }
106
107 fn active_window(&self) -> Option<AnyWindowHandle> {
108 self.mac_platform.active_window()
109 }
110
111 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
112 self.mac_platform.window_stack()
113 }
114
115 #[cfg(feature = "screen-capture")]
116 fn is_screen_capture_supported(&self) -> bool {
117 self.mac_platform.is_screen_capture_supported()
118 }
119
120 #[cfg(feature = "screen-capture")]
121 fn screen_capture_sources(
122 &self,
123 ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
124 self.mac_platform.screen_capture_sources()
125 }
126
127 fn open_window(
128 &self,
129 handle: AnyWindowHandle,
130 options: WindowParams,
131 ) -> Result<Box<dyn PlatformWindow>> {
132 self.mac_platform.open_window(handle, options)
133 }
134
135 fn window_appearance(&self) -> WindowAppearance {
136 self.mac_platform.window_appearance()
137 }
138
139 fn open_url(&self, url: &str) {
140 self.mac_platform.open_url(url)
141 }
142
143 fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {}
144
145 fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
146 Task::ready(Ok(()))
147 }
148
149 fn prompt_for_paths(
150 &self,
151 _options: PathPromptOptions,
152 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
153 let (tx, rx) = oneshot::channel();
154 tx.send(Ok(None)).ok();
155 rx
156 }
157
158 fn prompt_for_new_path(
159 &self,
160 _directory: &Path,
161 _suggested_name: Option<&str>,
162 ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
163 let (tx, rx) = oneshot::channel();
164 tx.send(Ok(None)).ok();
165 rx
166 }
167
168 fn can_select_mixed_files_and_dirs(&self) -> bool {
169 true
170 }
171
172 fn reveal_path(&self, path: &Path) {
173 self.mac_platform.reveal_path(path)
174 }
175
176 fn open_with_system(&self, path: &Path) {
177 self.mac_platform.open_with_system(path)
178 }
179
180 fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
181
182 fn on_reopen(&self, _callback: Box<dyn FnMut()>) {}
183
184 fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
185
186 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
187 None
188 }
189
190 fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
191
192 fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
193
194 fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
195
196 fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
197
198 fn app_path(&self) -> Result<PathBuf> {
199 self.mac_platform.app_path()
200 }
201
202 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
203 self.mac_platform.path_for_auxiliary_executable(name)
204 }
205
206 fn set_cursor_style(&self, style: CursorStyle) {
207 self.mac_platform.set_cursor_style(style)
208 }
209
210 fn should_auto_hide_scrollbars(&self) -> bool {
211 self.mac_platform.should_auto_hide_scrollbars()
212 }
213
214 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
215 self.clipboard.lock().clone()
216 }
217
218 fn write_to_clipboard(&self, item: ClipboardItem) {
219 *self.clipboard.lock() = Some(item);
220 }
221
222 #[cfg(target_os = "macos")]
223 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
224 self.find_pasteboard.lock().clone()
225 }
226
227 #[cfg(target_os = "macos")]
228 fn write_to_find_pasteboard(&self, item: ClipboardItem) {
229 *self.find_pasteboard.lock() = Some(item);
230 }
231
232 fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
233 Task::ready(Ok(()))
234 }
235
236 fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
237 Task::ready(Ok(None))
238 }
239
240 fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
241 Task::ready(Ok(()))
242 }
243
244 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
245 self.mac_platform.keyboard_layout()
246 }
247
248 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
249 self.mac_platform.keyboard_mapper()
250 }
251
252 fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {}
253}