visual_test.rs

  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}