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