visual_test_context.rs

  1use crate::{
  2    Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor,
  3    Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
  4    Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point,
  5    Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, Window,
  6    WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
  7};
  8use anyhow::anyhow;
  9use image::RgbaImage;
 10use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
 11
 12/// A test context that uses real macOS rendering instead of mocked rendering.
 13/// This is used for visual tests that need to capture actual screenshots.
 14///
 15/// Unlike `TestAppContext` which uses `TestPlatform` with mocked rendering,
 16/// `VisualTestAppContext` uses the real `MacPlatform` to produce actual rendered output.
 17///
 18/// Windows created through this context are positioned off-screen (at coordinates like -10000, -10000)
 19/// so they are invisible to the user but still fully rendered by the compositor.
 20#[derive(Clone)]
 21pub struct VisualTestAppContext {
 22    /// The underlying app cell
 23    pub app: Rc<AppCell>,
 24    /// The background executor for running async tasks
 25    pub background_executor: BackgroundExecutor,
 26    /// The foreground executor for running tasks on the main thread
 27    pub foreground_executor: ForegroundExecutor,
 28    /// The test dispatcher for deterministic task scheduling
 29    dispatcher: TestDispatcher,
 30    platform: Rc<dyn Platform>,
 31    text_system: Arc<TextSystem>,
 32}
 33
 34impl VisualTestAppContext {
 35    /// Creates a new `VisualTestAppContext` with real macOS platform rendering
 36    /// but deterministic task scheduling via TestDispatcher.
 37    ///
 38    /// This provides:
 39    /// - Real Metal/compositor rendering for accurate screenshots
 40    /// - Deterministic task scheduling via TestDispatcher
 41    /// - Controllable time via `advance_clock`
 42    ///
 43    /// Note: This uses a no-op asset source, so SVG icons won't render.
 44    /// Use `with_asset_source` to provide real assets for icon rendering.
 45    pub fn new() -> Self {
 46        Self::with_asset_source(Arc::new(()))
 47    }
 48
 49    /// Creates a new `VisualTestAppContext` with a custom asset source.
 50    ///
 51    /// Use this when you need SVG icons to render properly in visual tests.
 52    /// Pass the real `Assets` struct to enable icon rendering.
 53    pub fn with_asset_source(asset_source: Arc<dyn AssetSource>) -> Self {
 54        // Use a seeded RNG for deterministic behavior
 55        let seed = std::env::var("SEED")
 56            .ok()
 57            .and_then(|s| s.parse().ok())
 58            .unwrap_or(0);
 59
 60        // Create liveness for task cancellation
 61        let liveness = Arc::new(());
 62
 63        // Create a visual test platform that combines real Mac rendering
 64        // with controllable TestDispatcher for deterministic task scheduling
 65        let platform = Rc::new(VisualTestPlatform::new(seed, Arc::downgrade(&liveness)));
 66
 67        // Get the dispatcher and executors from the platform
 68        let dispatcher = platform.dispatcher().clone();
 69        let background_executor = platform.background_executor();
 70        let foreground_executor = platform.foreground_executor();
 71
 72        let text_system = Arc::new(TextSystem::new(platform.text_system()));
 73
 74        let http_client = http_client::FakeHttpClient::with_404_response();
 75
 76        let mut app = App::new_app(platform.clone(), liveness, asset_source, http_client);
 77        app.borrow_mut().mode = GpuiMode::test();
 78
 79        Self {
 80            app,
 81            background_executor,
 82            foreground_executor,
 83            dispatcher,
 84            platform,
 85            text_system,
 86        }
 87    }
 88
 89    /// Opens a window positioned off-screen for invisible rendering.
 90    ///
 91    /// The window is positioned at (-10000, -10000) so it's not visible on any display,
 92    /// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit.
 93    ///
 94    /// # Arguments
 95    /// * `size` - The size of the window to create
 96    /// * `build_root` - A closure that builds the root view for the window
 97    pub fn open_offscreen_window<V: Render + 'static>(
 98        &mut self,
 99        size: Size<Pixels>,
100        build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
101    ) -> Result<WindowHandle<V>> {
102        use crate::{point, px};
103
104        let bounds = Bounds {
105            origin: point(px(-10000.0), px(-10000.0)),
106            size,
107        };
108
109        let mut cx = self.app.borrow_mut();
110        cx.open_window(
111            WindowOptions {
112                window_bounds: Some(WindowBounds::Windowed(bounds)),
113                focus: false,
114                show: true,
115                ..Default::default()
116            },
117            build_root,
118        )
119    }
120
121    /// Opens an off-screen window with default size (1280x800).
122    pub fn open_offscreen_window_default<V: Render + 'static>(
123        &mut self,
124        build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
125    ) -> Result<WindowHandle<V>> {
126        use crate::{px, size};
127        self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root)
128    }
129
130    /// Returns whether screen capture is supported on this platform.
131    pub fn is_screen_capture_supported(&self) -> bool {
132        self.platform.is_screen_capture_supported()
133    }
134
135    /// Returns the text system used by this context.
136    pub fn text_system(&self) -> &Arc<TextSystem> {
137        &self.text_system
138    }
139
140    /// Returns the background executor.
141    pub fn executor(&self) -> BackgroundExecutor {
142        self.background_executor.clone()
143    }
144
145    /// Returns the foreground executor.
146    pub fn foreground_executor(&self) -> ForegroundExecutor {
147        self.foreground_executor.clone()
148    }
149
150    /// Runs all pending foreground and background tasks until there's nothing left to do.
151    /// This is essential for processing async operations like tooltip timers.
152    pub fn run_until_parked(&self) {
153        self.dispatcher.run_until_parked();
154    }
155
156    /// Advances the simulated clock by the given duration and processes any tasks
157    /// that become ready. This is essential for testing time-based behaviors like
158    /// tooltip delays.
159    pub fn advance_clock(&self, duration: Duration) {
160        self.dispatcher.advance_clock(duration);
161    }
162
163    /// Updates the app state.
164    pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
165        let mut app = self.app.borrow_mut();
166        f(&mut app)
167    }
168
169    /// Reads from the app state.
170    pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
171        let app = self.app.borrow();
172        f(&app)
173    }
174
175    /// Updates a window.
176    pub fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
177    where
178        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
179    {
180        let mut lock = self.app.borrow_mut();
181        lock.update_window(window, f)
182    }
183
184    /// Spawns a task on the foreground executor.
185    pub fn spawn<F, R>(&self, f: F) -> Task<R>
186    where
187        F: Future<Output = R> + 'static,
188        R: 'static,
189    {
190        self.foreground_executor.spawn(f)
191    }
192
193    /// Checks if a global of type G exists.
194    pub fn has_global<G: Global>(&self) -> bool {
195        let app = self.app.borrow();
196        app.has_global::<G>()
197    }
198
199    /// Reads a global value.
200    pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
201        let app = self.app.borrow();
202        f(app.global::<G>(), &app)
203    }
204
205    /// Sets a global value.
206    pub fn set_global<G: Global>(&mut self, global: G) {
207        let mut app = self.app.borrow_mut();
208        app.set_global(global);
209    }
210
211    /// Updates a global value.
212    pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
213        let mut lock = self.app.borrow_mut();
214        lock.update(|cx| {
215            let mut global = cx.lease_global::<G>();
216            let result = f(&mut global, cx);
217            cx.end_global_lease(global);
218            result
219        })
220    }
221
222    /// Simulates a sequence of keystrokes on the given window.
223    ///
224    /// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape".
225    pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
226        for keystroke_text in keystrokes.split_whitespace() {
227            let keystroke = Keystroke::parse(keystroke_text)
228                .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text));
229            self.dispatch_keystroke(window, keystroke);
230        }
231        self.run_until_parked();
232    }
233
234    /// Dispatches a single keystroke to a window.
235    pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
236        self.update_window(window, |_, window, cx| {
237            window.dispatch_keystroke(keystroke, cx);
238        })
239        .ok();
240    }
241
242    /// Simulates typing text input on the given window.
243    pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
244        for char in input.chars() {
245            let key = char.to_string();
246            let keystroke = Keystroke {
247                modifiers: Modifiers::default(),
248                key: key.clone(),
249                key_char: Some(key),
250            };
251            self.dispatch_keystroke(window, keystroke);
252        }
253        self.run_until_parked();
254    }
255
256    /// Simulates a mouse move event.
257    pub fn simulate_mouse_move(
258        &mut self,
259        window: AnyWindowHandle,
260        position: Point<Pixels>,
261        button: impl Into<Option<MouseButton>>,
262        modifiers: Modifiers,
263    ) {
264        self.simulate_event(
265            window,
266            MouseMoveEvent {
267                position,
268                modifiers,
269                pressed_button: button.into(),
270            },
271        );
272    }
273
274    /// Simulates a mouse down event.
275    pub fn simulate_mouse_down(
276        &mut self,
277        window: AnyWindowHandle,
278        position: Point<Pixels>,
279        button: MouseButton,
280        modifiers: Modifiers,
281    ) {
282        self.simulate_event(
283            window,
284            MouseDownEvent {
285                position,
286                modifiers,
287                button,
288                click_count: 1,
289                first_mouse: false,
290            },
291        );
292    }
293
294    /// Simulates a mouse up event.
295    pub fn simulate_mouse_up(
296        &mut self,
297        window: AnyWindowHandle,
298        position: Point<Pixels>,
299        button: MouseButton,
300        modifiers: Modifiers,
301    ) {
302        self.simulate_event(
303            window,
304            MouseUpEvent {
305                position,
306                modifiers,
307                button,
308                click_count: 1,
309            },
310        );
311    }
312
313    /// Simulates a click (mouse down followed by mouse up).
314    pub fn simulate_click(
315        &mut self,
316        window: AnyWindowHandle,
317        position: Point<Pixels>,
318        modifiers: Modifiers,
319    ) {
320        self.simulate_mouse_down(window, position, MouseButton::Left, modifiers);
321        self.simulate_mouse_up(window, position, MouseButton::Left, modifiers);
322    }
323
324    /// Simulates an input event on the given window.
325    pub fn simulate_event<E: InputEvent>(&mut self, window: AnyWindowHandle, event: E) {
326        self.update_window(window, |_, window, cx| {
327            window.dispatch_event(event.to_platform_input(), cx);
328        })
329        .ok();
330        self.run_until_parked();
331    }
332
333    /// Dispatches an action to the given window.
334    pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) {
335        self.update_window(window, |_, window, cx| {
336            window.dispatch_action(action.boxed_clone(), cx);
337        })
338        .ok();
339        self.run_until_parked();
340    }
341
342    /// Writes to the clipboard.
343    pub fn write_to_clipboard(&self, item: ClipboardItem) {
344        self.platform.write_to_clipboard(item);
345    }
346
347    /// Reads from the clipboard.
348    pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
349        self.platform.read_from_clipboard()
350    }
351
352    /// Waits for a condition to become true, with a timeout.
353    pub async fn wait_for<T: 'static>(
354        &mut self,
355        entity: &Entity<T>,
356        predicate: impl Fn(&T) -> bool,
357        timeout: Duration,
358    ) -> Result<()> {
359        let start = std::time::Instant::now();
360        loop {
361            {
362                let app = self.app.borrow();
363                if predicate(entity.read(&app)) {
364                    return Ok(());
365                }
366            }
367
368            if start.elapsed() > timeout {
369                return Err(anyhow!("Timed out waiting for condition"));
370            }
371
372            self.run_until_parked();
373            self.background_executor
374                .timer(Duration::from_millis(10))
375                .await;
376        }
377    }
378
379    /// Captures a screenshot of the specified window using direct texture capture.
380    ///
381    /// This renders the scene to a Metal texture and reads the pixels directly,
382    /// which does not require the window to be visible on screen.
383    #[cfg(any(test, feature = "test-support"))]
384    pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
385        self.update_window(window, |_, window, _cx| window.render_to_image())?
386    }
387
388    /// Waits for animations to complete by waiting a couple of frames.
389    pub async fn wait_for_animations(&self) {
390        self.background_executor
391            .timer(Duration::from_millis(32))
392            .await;
393        self.run_until_parked();
394    }
395}
396
397impl Default for VisualTestAppContext {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403impl AppContext for VisualTestAppContext {
404    fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
405        let mut app = self.app.borrow_mut();
406        app.new(build_entity)
407    }
408
409    fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
410        let mut app = self.app.borrow_mut();
411        app.reserve_entity()
412    }
413
414    fn insert_entity<T: 'static>(
415        &mut self,
416        reservation: crate::Reservation<T>,
417        build_entity: impl FnOnce(&mut Context<T>) -> T,
418    ) -> Entity<T> {
419        let mut app = self.app.borrow_mut();
420        app.insert_entity(reservation, build_entity)
421    }
422
423    fn update_entity<T: 'static, R>(
424        &mut self,
425        handle: &Entity<T>,
426        update: impl FnOnce(&mut T, &mut Context<T>) -> R,
427    ) -> R {
428        let mut app = self.app.borrow_mut();
429        app.update_entity(handle, update)
430    }
431
432    fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> crate::GpuiBorrow<'a, T>
433    where
434        T: 'static,
435    {
436        panic!("Cannot use as_mut with a visual test app context. Try calling update() first")
437    }
438
439    fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
440    where
441        T: 'static,
442    {
443        let app = self.app.borrow();
444        app.read_entity(handle, read)
445    }
446
447    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
448    where
449        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
450    {
451        let mut lock = self.app.borrow_mut();
452        lock.update_window(window, f)
453    }
454
455    fn read_window<T, R>(
456        &self,
457        window: &WindowHandle<T>,
458        read: impl FnOnce(Entity<T>, &App) -> R,
459    ) -> Result<R>
460    where
461        T: 'static,
462    {
463        let app = self.app.borrow();
464        app.read_window(window, read)
465    }
466
467    fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
468    where
469        R: Send + 'static,
470    {
471        self.background_executor.spawn(future)
472    }
473
474    fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
475    where
476        G: Global,
477    {
478        let app = self.app.borrow();
479        callback(app.global::<G>(), &app)
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use crate::Empty;
487    use std::cell::RefCell;
488
489    // Note: All VisualTestAppContext tests are ignored by default because they require
490    // the macOS main thread. Standard Rust tests run on worker threads, which causes
491    // SIGABRT when interacting with macOS AppKit/Cocoa APIs.
492    //
493    // To run these tests, use:
494    // cargo test -p gpui visual_test_context -- --ignored --test-threads=1
495
496    #[test]
497    #[ignore] // Requires macOS main thread
498    fn test_foreground_tasks_run_with_run_until_parked() {
499        let mut cx = VisualTestAppContext::new();
500
501        let task_ran = Rc::new(RefCell::new(false));
502
503        // Spawn a foreground task via the App's spawn method
504        // This should use our TestDispatcher, not the MacDispatcher
505        {
506            let task_ran = task_ran.clone();
507            cx.update(|cx| {
508                cx.spawn(async move |_| {
509                    *task_ran.borrow_mut() = true;
510                })
511                .detach();
512            });
513        }
514
515        // The task should not have run yet
516        assert!(!*task_ran.borrow());
517
518        // Run until parked should execute the foreground task
519        cx.run_until_parked();
520
521        // Now the task should have run
522        assert!(*task_ran.borrow());
523    }
524
525    #[test]
526    #[ignore] // Requires macOS main thread
527    fn test_advance_clock_triggers_delayed_tasks() {
528        let mut cx = VisualTestAppContext::new();
529
530        let task_ran = Rc::new(RefCell::new(false));
531
532        // Spawn a task that waits for a timer
533        {
534            let task_ran = task_ran.clone();
535            let executor = cx.background_executor.clone();
536            cx.update(|cx| {
537                cx.spawn(async move |_| {
538                    executor.timer(Duration::from_millis(500)).await;
539                    *task_ran.borrow_mut() = true;
540                })
541                .detach();
542            });
543        }
544
545        // Run until parked - the task should be waiting on the timer
546        cx.run_until_parked();
547        assert!(!*task_ran.borrow());
548
549        // Advance clock past the timer duration
550        cx.advance_clock(Duration::from_millis(600));
551
552        // Now the task should have completed
553        assert!(*task_ran.borrow());
554    }
555
556    #[test]
557    #[ignore] // Requires macOS main thread - window creation fails on test threads
558    fn test_window_spawn_uses_test_dispatcher() {
559        let mut cx = VisualTestAppContext::new();
560
561        let task_ran = Rc::new(RefCell::new(false));
562
563        let window = cx
564            .open_offscreen_window_default(|_, cx| cx.new(|_| Empty))
565            .expect("Failed to open window");
566
567        // Spawn a task via window.spawn - this is the critical test case
568        // for tooltip behavior, as tooltips use window.spawn for delayed show
569        {
570            let task_ran = task_ran.clone();
571            cx.update_window(window.into(), |_, window, cx| {
572                window
573                    .spawn(cx, async move |_| {
574                        *task_ran.borrow_mut() = true;
575                    })
576                    .detach();
577            })
578            .ok();
579        }
580
581        // The task should not have run yet
582        assert!(!*task_ran.borrow());
583
584        // Run until parked should execute the foreground task spawned via window
585        cx.run_until_parked();
586
587        // Now the task should have run
588        assert!(*task_ran.borrow());
589    }
590}