263 lines
7.5 KiB
Rust
263 lines
7.5 KiB
Rust
//! Visual test platform that combines real rendering (macOs-only for now) with controllable TestDispatcher.
|
|
//!
|
|
//! This platform is used for visual tests that need:
|
|
//! - Real rendering (e.g. Metal/compositor) for accurate screenshots
|
|
//! - Deterministic task scheduling via TestDispatcher
|
|
//! - Controllable time via `advance_clock`
|
|
|
|
use crate::ScreenCaptureSource;
|
|
use crate::{
|
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, Keymap,
|
|
Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
|
|
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
|
|
TestDispatcher, WindowAppearance, WindowParams,
|
|
};
|
|
use anyhow::Result;
|
|
use futures::channel::oneshot;
|
|
use parking_lot::Mutex;
|
|
|
|
use std::{
|
|
path::{Path, PathBuf},
|
|
rc::Rc,
|
|
sync::Arc,
|
|
};
|
|
|
|
/// A platform that combines real Mac rendering with controllable TestDispatcher.
|
|
///
|
|
/// This allows visual tests to:
|
|
/// - Render real UI via Metal for accurate screenshots
|
|
/// - Control task scheduling deterministically via TestDispatcher
|
|
/// - Advance simulated time for testing time-based behaviors (tooltips, animations, etc.)
|
|
pub struct VisualTestPlatform {
|
|
dispatcher: TestDispatcher,
|
|
background_executor: BackgroundExecutor,
|
|
foreground_executor: ForegroundExecutor,
|
|
platform: Rc<dyn Platform>,
|
|
clipboard: Mutex<Option<ClipboardItem>>,
|
|
find_pasteboard: Mutex<Option<ClipboardItem>>,
|
|
}
|
|
|
|
impl VisualTestPlatform {
|
|
/// Creates a new VisualTestPlatform with the given random seed.
|
|
///
|
|
/// The seed is used for deterministic random number generation in the TestDispatcher.
|
|
pub fn new(platform: Rc<dyn Platform>, seed: u64) -> Self {
|
|
let dispatcher = TestDispatcher::new(seed);
|
|
let arc_dispatcher = Arc::new(dispatcher.clone());
|
|
|
|
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
|
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
|
|
|
Self {
|
|
dispatcher,
|
|
background_executor,
|
|
foreground_executor,
|
|
platform,
|
|
clipboard: Mutex::new(None),
|
|
find_pasteboard: Mutex::new(None),
|
|
}
|
|
}
|
|
|
|
/// Returns a reference to the TestDispatcher for controlling task scheduling and time.
|
|
pub fn dispatcher(&self) -> &TestDispatcher {
|
|
&self.dispatcher
|
|
}
|
|
}
|
|
|
|
impl Platform for VisualTestPlatform {
|
|
fn background_executor(&self) -> BackgroundExecutor {
|
|
self.background_executor.clone()
|
|
}
|
|
|
|
fn foreground_executor(&self) -> ForegroundExecutor {
|
|
self.foreground_executor.clone()
|
|
}
|
|
|
|
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
|
self.platform.text_system()
|
|
}
|
|
|
|
fn run(&self, _on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
|
panic!("VisualTestPlatform::run should not be called in tests")
|
|
}
|
|
|
|
fn quit(&self) {}
|
|
|
|
fn restart(&self, _binary_path: Option<PathBuf>) {}
|
|
|
|
fn activate(&self, _ignoring_other_apps: bool) {}
|
|
|
|
fn hide(&self) {}
|
|
|
|
fn hide_other_apps(&self) {}
|
|
|
|
fn unhide_other_apps(&self) {}
|
|
|
|
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
|
self.platform.displays()
|
|
}
|
|
|
|
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
|
self.platform.primary_display()
|
|
}
|
|
|
|
fn active_window(&self) -> Option<AnyWindowHandle> {
|
|
self.platform.active_window()
|
|
}
|
|
|
|
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
|
|
self.platform.window_stack()
|
|
}
|
|
|
|
fn is_screen_capture_supported(&self) -> bool {
|
|
self.platform.is_screen_capture_supported()
|
|
}
|
|
|
|
fn screen_capture_sources(
|
|
&self,
|
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
|
self.platform.screen_capture_sources()
|
|
}
|
|
|
|
fn open_window(
|
|
&self,
|
|
handle: AnyWindowHandle,
|
|
options: WindowParams,
|
|
) -> Result<Box<dyn PlatformWindow>> {
|
|
self.platform.open_window(handle, options)
|
|
}
|
|
|
|
fn window_appearance(&self) -> WindowAppearance {
|
|
self.platform.window_appearance()
|
|
}
|
|
|
|
fn open_url(&self, url: &str) {
|
|
self.platform.open_url(url)
|
|
}
|
|
|
|
fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {}
|
|
|
|
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
|
|
Task::ready(Ok(()))
|
|
}
|
|
|
|
fn prompt_for_paths(
|
|
&self,
|
|
_options: PathPromptOptions,
|
|
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
|
let (tx, rx) = oneshot::channel();
|
|
tx.send(Ok(None)).ok();
|
|
rx
|
|
}
|
|
|
|
fn prompt_for_new_path(
|
|
&self,
|
|
_directory: &Path,
|
|
_suggested_name: Option<&str>,
|
|
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
|
let (tx, rx) = oneshot::channel();
|
|
tx.send(Ok(None)).ok();
|
|
rx
|
|
}
|
|
|
|
fn can_select_mixed_files_and_dirs(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn reveal_path(&self, path: &Path) {
|
|
self.platform.reveal_path(path)
|
|
}
|
|
|
|
fn open_with_system(&self, path: &Path) {
|
|
self.platform.open_with_system(path)
|
|
}
|
|
|
|
fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
|
|
|
|
fn on_reopen(&self, _callback: Box<dyn FnMut()>) {}
|
|
|
|
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
|
|
|
|
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
|
None
|
|
}
|
|
|
|
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
|
|
|
|
fn on_app_menu_action(&self, _callback: Box<dyn FnMut(&dyn crate::Action)>) {}
|
|
|
|
fn on_will_open_app_menu(&self, _callback: Box<dyn FnMut()>) {}
|
|
|
|
fn on_validate_app_menu_command(&self, _callback: Box<dyn FnMut(&dyn crate::Action) -> bool>) {}
|
|
|
|
fn app_path(&self) -> Result<PathBuf> {
|
|
self.platform.app_path()
|
|
}
|
|
|
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
|
self.platform.path_for_auxiliary_executable(name)
|
|
}
|
|
|
|
fn set_cursor_style(&self, style: CursorStyle) {
|
|
self.platform.set_cursor_style(style)
|
|
}
|
|
|
|
fn hide_cursor_until_mouse_moves(&self) {
|
|
self.platform.hide_cursor_until_mouse_moves();
|
|
}
|
|
|
|
fn is_cursor_visible(&self) -> bool {
|
|
self.platform.is_cursor_visible()
|
|
}
|
|
|
|
fn should_auto_hide_scrollbars(&self) -> bool {
|
|
self.platform.should_auto_hide_scrollbars()
|
|
}
|
|
|
|
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
|
self.clipboard.lock().clone()
|
|
}
|
|
|
|
fn write_to_clipboard(&self, item: ClipboardItem) {
|
|
*self.clipboard.lock() = Some(item);
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
|
self.find_pasteboard.lock().clone()
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
|
*self.find_pasteboard.lock() = Some(item);
|
|
}
|
|
|
|
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
|
|
Task::ready(Ok(()))
|
|
}
|
|
|
|
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
|
|
Task::ready(Ok(None))
|
|
}
|
|
|
|
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
|
|
Task::ready(Ok(()))
|
|
}
|
|
|
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
|
self.platform.keyboard_layout()
|
|
}
|
|
|
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
|
self.platform.keyboard_mapper()
|
|
}
|
|
|
|
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {}
|
|
|
|
fn thermal_state(&self) -> super::ThermalState {
|
|
super::ThermalState::Nominal
|
|
}
|
|
|
|
fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
|
|
}
|