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