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
  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}