use crate::{ Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EntityId, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode, }; use anyhow::anyhow; use image::RgbaImage; use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; /// A test context that uses real macOS rendering instead of mocked rendering. /// This is used for visual tests that need to capture actual screenshots. /// /// Unlike `TestAppContext` which uses `TestPlatform` with mocked rendering, /// `VisualTestAppContext` uses the real `MacPlatform` to produce actual rendered output. /// /// Windows created through this context are positioned off-screen (at coordinates like -10000, -10000) /// so they are invisible to the user but still fully rendered by the compositor. #[derive(Clone)] pub struct VisualTestAppContext { /// The underlying app cell pub app: Rc, /// The background executor for running async tasks pub background_executor: BackgroundExecutor, /// The foreground executor for running tasks on the main thread pub foreground_executor: ForegroundExecutor, /// The test dispatcher for deterministic task scheduling dispatcher: TestDispatcher, platform: Rc, text_system: Arc, } impl VisualTestAppContext { /// Creates a new `VisualTestAppContext` with real macOS platform rendering /// but deterministic task scheduling via TestDispatcher. /// /// This provides: /// - Real Metal/compositor rendering for accurate screenshots /// - Deterministic task scheduling via TestDispatcher /// - Controllable time via `advance_clock` /// /// Note: This uses a no-op asset source, so SVG icons won't render. /// Use `with_asset_source` to provide real assets for icon rendering. pub fn new(platform: Rc) -> Self { Self::with_asset_source(platform, Arc::new(())) } /// Creates a new `VisualTestAppContext` with a custom asset source. /// /// Use this when you need SVG icons to render properly in visual tests. /// Pass the real `Assets` struct to enable icon rendering. pub fn with_asset_source( platform: Rc, asset_source: Arc, ) -> Self { // Use a seeded RNG for deterministic behavior let seed = std::env::var("SEED") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(0); // Create a visual test platform that combines real Mac rendering // with controllable TestDispatcher for deterministic task scheduling let platform = Rc::new(VisualTestPlatform::new(platform, seed)); // Get the dispatcher and executors from the platform let dispatcher = platform.dispatcher().clone(); let background_executor = platform.background_executor(); let foreground_executor = platform.foreground_executor(); let text_system = Arc::new(TextSystem::new(platform.text_system())); let http_client = http_client::FakeHttpClient::with_404_response(); let mut app = App::new_app(platform.clone(), asset_source, http_client); app.borrow_mut().mode = GpuiMode::test(); Self { app, background_executor, foreground_executor, dispatcher, platform, text_system, } } /// Opens a window positioned off-screen for invisible rendering. /// /// The window is positioned at (-10000, -10000) so it's not visible on any display, /// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit. /// /// # Arguments /// * `size` - The size of the window to create /// * `build_root` - A closure that builds the root view for the window pub fn open_offscreen_window( &mut self, size: Size, build_root: impl FnOnce(&mut Window, &mut App) -> Entity, ) -> Result> { use crate::{point, px}; let bounds = Bounds { origin: point(px(-10000.0), px(-10000.0)), size, }; let mut cx = self.app.borrow_mut(); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), focus: false, show: true, ..Default::default() }, build_root, ) } /// Opens an off-screen window with default size (1280x800). pub fn open_offscreen_window_default( &mut self, build_root: impl FnOnce(&mut Window, &mut App) -> Entity, ) -> Result> { use crate::{px, size}; self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root) } /// Returns whether screen capture is supported on this platform. pub fn is_screen_capture_supported(&self) -> bool { self.platform.is_screen_capture_supported() } /// Returns the text system used by this context. pub fn text_system(&self) -> &Arc { &self.text_system } /// Returns the background executor. pub fn executor(&self) -> BackgroundExecutor { self.background_executor.clone() } /// Returns the foreground executor. pub fn foreground_executor(&self) -> ForegroundExecutor { self.foreground_executor.clone() } /// Runs all pending foreground and background tasks until there's nothing left to do. /// This is essential for processing async operations like tooltip timers. pub fn run_until_parked(&self) { self.dispatcher.run_until_parked(); } /// Advances the simulated clock by the given duration and processes any tasks /// that become ready. This is essential for testing time-based behaviors like /// tooltip delays. pub fn advance_clock(&self, duration: Duration) { self.dispatcher.advance_clock(duration); } /// Updates the app state. pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { let mut app = self.app.borrow_mut(); f(&mut app) } /// Reads from the app state. pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { let app = self.app.borrow(); f(&app) } /// Updates a window. pub fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut Window, &mut App) -> T, { let mut lock = self.app.borrow_mut(); lock.update_window(window, f) } /// Spawns a task on the foreground executor. pub fn spawn(&self, f: F) -> Task where F: Future + 'static, R: 'static, { self.foreground_executor.spawn(f) } /// Checks if a global of type G exists. pub fn has_global(&self) -> bool { let app = self.app.borrow(); app.has_global::() } /// Reads a global value. pub fn read_global(&self, f: impl FnOnce(&G, &App) -> R) -> R { let app = self.app.borrow(); f(app.global::(), &app) } /// Sets a global value. pub fn set_global(&mut self, global: G) { let mut app = self.app.borrow_mut(); app.set_global(global); } /// Updates a global value. pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R { let mut lock = self.app.borrow_mut(); lock.update(|cx| { let mut global = cx.lease_global::(); let result = f(&mut global, cx); cx.end_global_lease(global); result }) } /// Simulates a sequence of keystrokes on the given window. /// /// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape". pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { for keystroke_text in keystrokes.split_whitespace() { let keystroke = Keystroke::parse(keystroke_text) .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text)); self.dispatch_keystroke(window, keystroke); } self.run_until_parked(); } /// Dispatches a single keystroke to a window. pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { self.update_window(window, |_, window, cx| { window.dispatch_keystroke(keystroke, cx); }) .ok(); } /// Simulates typing text input on the given window. pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { for char in input.chars() { let key = char.to_string(); let keystroke = Keystroke { modifiers: Modifiers::default(), key: key.clone(), key_char: Some(key), }; self.dispatch_keystroke(window, keystroke); } self.run_until_parked(); } /// Simulates a mouse move event. pub fn simulate_mouse_move( &mut self, window: AnyWindowHandle, position: Point, button: impl Into>, modifiers: Modifiers, ) { self.simulate_event( window, MouseMoveEvent { position, modifiers, pressed_button: button.into(), }, ); } /// Simulates a mouse down event. pub fn simulate_mouse_down( &mut self, window: AnyWindowHandle, position: Point, button: MouseButton, modifiers: Modifiers, ) { self.simulate_event( window, MouseDownEvent { position, modifiers, button, click_count: 1, first_mouse: false, }, ); } /// Simulates a mouse up event. pub fn simulate_mouse_up( &mut self, window: AnyWindowHandle, position: Point, button: MouseButton, modifiers: Modifiers, ) { self.simulate_event( window, MouseUpEvent { position, modifiers, button, click_count: 1, }, ); } /// Simulates a click (mouse down followed by mouse up). pub fn simulate_click( &mut self, window: AnyWindowHandle, position: Point, modifiers: Modifiers, ) { self.simulate_mouse_down(window, position, MouseButton::Left, modifiers); self.simulate_mouse_up(window, position, MouseButton::Left, modifiers); } /// Simulates an input event on the given window. pub fn simulate_event(&mut self, window: AnyWindowHandle, event: E) { self.update_window(window, |_, window, cx| { window.dispatch_event(event.to_platform_input(), cx); }) .ok(); self.run_until_parked(); } /// Dispatches an action to the given window. pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) { self.update_window(window, |_, window, cx| { window.dispatch_action(action.boxed_clone(), cx); }) .ok(); self.run_until_parked(); } /// Writes to the clipboard. pub fn write_to_clipboard(&self, item: ClipboardItem) { self.platform.write_to_clipboard(item); } /// Reads from the clipboard. pub fn read_from_clipboard(&self) -> Option { self.platform.read_from_clipboard() } /// Waits for a condition to become true, with a timeout. pub async fn wait_for( &mut self, entity: &Entity, predicate: impl Fn(&T) -> bool, timeout: Duration, ) -> Result<()> { let start = web_time::Instant::now(); loop { { let app = self.app.borrow(); if predicate(entity.read(&app)) { return Ok(()); } } if start.elapsed() > timeout { return Err(anyhow!("Timed out waiting for condition")); } self.run_until_parked(); self.background_executor .timer(Duration::from_millis(10)) .await; } } /// Captures a screenshot of the specified window using direct texture capture. /// /// This renders the scene to a Metal texture and reads the pixels directly, /// which does not require the window to be visible on screen. #[cfg(any(test, feature = "test-support"))] pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result { self.update_window(window, |_, window, _cx| window.render_to_image())? } /// Waits for animations to complete by waiting a couple of frames. pub async fn wait_for_animations(&self) { self.background_executor .timer(Duration::from_millis(32)) .await; self.run_until_parked(); } } impl AppContext for VisualTestAppContext { fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { let mut app = self.app.borrow_mut(); app.new(build_entity) } fn reserve_entity(&mut self) -> crate::Reservation { let mut app = self.app.borrow_mut(); app.reserve_entity() } fn insert_entity( &mut self, reservation: crate::Reservation, build_entity: impl FnOnce(&mut Context) -> T, ) -> Entity { let mut app = self.app.borrow_mut(); app.insert_entity(reservation, build_entity) } fn update_entity( &mut self, handle: &Entity, update: impl FnOnce(&mut T, &mut Context) -> R, ) -> R { let mut app = self.app.borrow_mut(); app.update_entity(handle, update) } fn as_mut<'a, T>(&'a mut self, _: &Entity) -> crate::GpuiBorrow<'a, T> where T: 'static, { panic!("Cannot use as_mut with a visual test app context. Try calling update() first") } fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R where T: 'static, { let app = self.app.borrow(); app.read_entity(handle, read) } fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut Window, &mut App) -> T, { let mut lock = self.app.borrow_mut(); lock.update_window(window, f) } fn with_window( &mut self, entity_id: EntityId, f: impl FnOnce(&mut Window, &mut App) -> R, ) -> Option { let mut lock = self.app.borrow_mut(); lock.with_window(entity_id, f) } fn read_window( &self, window: &WindowHandle, read: impl FnOnce(Entity, &App) -> R, ) -> Result where T: 'static, { let app = self.app.borrow(); app.read_window(window, read) } fn background_spawn(&self, future: impl Future + Send + 'static) -> Task where R: Send + 'static, { self.background_executor.spawn(future) } fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R where G: Global, { let app = self.app.borrow(); callback(app.global::(), &app) } }