visual_test_context.rs

  1use crate::{
  2    Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, Bounds,
  3    ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
  4    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
  5    Result, Size, Task, TextSystem, Window, WindowBounds, WindowHandle, WindowOptions,
  6    app::GpuiMode, current_platform,
  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    platform: Rc<dyn Platform>,
 29    text_system: Arc<TextSystem>,
 30}
 31
 32impl VisualTestAppContext {
 33    /// Creates a new `VisualTestAppContext` with real macOS platform rendering.
 34    ///
 35    /// This initializes the real macOS platform (not the test platform), which means:
 36    /// - Windows are actually rendered by Metal/the compositor
 37    /// - Screenshots can be captured via ScreenCaptureKit
 38    /// - All platform APIs work as they do in production
 39    pub fn new() -> Self {
 40        let platform = current_platform(false);
 41        let background_executor = platform.background_executor();
 42        let foreground_executor = platform.foreground_executor();
 43        let text_system = Arc::new(TextSystem::new(platform.text_system()));
 44
 45        let asset_source = Arc::new(());
 46        let http_client = http_client::FakeHttpClient::with_404_response();
 47
 48        let mut app = App::new_app(platform.clone(), asset_source, http_client);
 49        app.borrow_mut().mode = GpuiMode::test();
 50
 51        Self {
 52            app,
 53            background_executor,
 54            foreground_executor,
 55            platform,
 56            text_system,
 57        }
 58    }
 59
 60    /// Opens a window positioned off-screen for invisible rendering.
 61    ///
 62    /// The window is positioned at (-10000, -10000) so it's not visible on any display,
 63    /// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit.
 64    ///
 65    /// # Arguments
 66    /// * `size` - The size of the window to create
 67    /// * `build_root` - A closure that builds the root view for the window
 68    pub fn open_offscreen_window<V: Render + 'static>(
 69        &mut self,
 70        size: Size<Pixels>,
 71        build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
 72    ) -> Result<WindowHandle<V>> {
 73        use crate::{point, px};
 74
 75        let bounds = Bounds {
 76            origin: point(px(-10000.0), px(-10000.0)),
 77            size,
 78        };
 79
 80        let mut cx = self.app.borrow_mut();
 81        cx.open_window(
 82            WindowOptions {
 83                window_bounds: Some(WindowBounds::Windowed(bounds)),
 84                focus: false,
 85                show: true,
 86                ..Default::default()
 87            },
 88            build_root,
 89        )
 90    }
 91
 92    /// Opens an off-screen window with default size (1280x800).
 93    pub fn open_offscreen_window_default<V: Render + 'static>(
 94        &mut self,
 95        build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
 96    ) -> Result<WindowHandle<V>> {
 97        use crate::{px, size};
 98        self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root)
 99    }
100
101    /// Returns whether screen capture is supported on this platform.
102    pub fn is_screen_capture_supported(&self) -> bool {
103        self.platform.is_screen_capture_supported()
104    }
105
106    /// Returns the text system used by this context.
107    pub fn text_system(&self) -> &Arc<TextSystem> {
108        &self.text_system
109    }
110
111    /// Returns the background executor.
112    pub fn executor(&self) -> BackgroundExecutor {
113        self.background_executor.clone()
114    }
115
116    /// Returns the foreground executor.
117    pub fn foreground_executor(&self) -> ForegroundExecutor {
118        self.foreground_executor.clone()
119    }
120
121    /// Runs pending background tasks until there's nothing left to do.
122    pub fn run_until_parked(&self) {
123        self.background_executor.run_until_parked();
124    }
125
126    /// Updates the app state.
127    pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
128        let mut app = self.app.borrow_mut();
129        f(&mut app)
130    }
131
132    /// Reads from the app state.
133    pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
134        let app = self.app.borrow();
135        f(&app)
136    }
137
138    /// Updates a window.
139    pub fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
140    where
141        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
142    {
143        let mut lock = self.app.borrow_mut();
144        lock.update_window(window, f)
145    }
146
147    /// Spawns a task on the foreground executor.
148    pub fn spawn<F, R>(&self, f: F) -> Task<R>
149    where
150        F: Future<Output = R> + 'static,
151        R: 'static,
152    {
153        self.foreground_executor.spawn(f)
154    }
155
156    /// Checks if a global of type G exists.
157    pub fn has_global<G: Global>(&self) -> bool {
158        let app = self.app.borrow();
159        app.has_global::<G>()
160    }
161
162    /// Reads a global value.
163    pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
164        let app = self.app.borrow();
165        f(app.global::<G>(), &app)
166    }
167
168    /// Sets a global value.
169    pub fn set_global<G: Global>(&mut self, global: G) {
170        let mut app = self.app.borrow_mut();
171        app.set_global(global);
172    }
173
174    /// Updates a global value.
175    pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
176        let mut lock = self.app.borrow_mut();
177        lock.update(|cx| {
178            let mut global = cx.lease_global::<G>();
179            let result = f(&mut global, cx);
180            cx.end_global_lease(global);
181            result
182        })
183    }
184
185    /// Simulates a sequence of keystrokes on the given window.
186    ///
187    /// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape".
188    pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
189        for keystroke_text in keystrokes.split_whitespace() {
190            let keystroke = Keystroke::parse(keystroke_text)
191                .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text));
192            self.dispatch_keystroke(window, keystroke);
193        }
194        self.run_until_parked();
195    }
196
197    /// Dispatches a single keystroke to a window.
198    pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
199        self.update_window(window, |_, window, cx| {
200            window.dispatch_keystroke(keystroke, cx);
201        })
202        .ok();
203    }
204
205    /// Simulates typing text input on the given window.
206    pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
207        for char in input.chars() {
208            let key = char.to_string();
209            let keystroke = Keystroke {
210                modifiers: Modifiers::default(),
211                key: key.clone(),
212                key_char: Some(key),
213            };
214            self.dispatch_keystroke(window, keystroke);
215        }
216        self.run_until_parked();
217    }
218
219    /// Simulates a mouse move event.
220    pub fn simulate_mouse_move(
221        &mut self,
222        window: AnyWindowHandle,
223        position: Point<Pixels>,
224        button: impl Into<Option<MouseButton>>,
225        modifiers: Modifiers,
226    ) {
227        self.simulate_event(
228            window,
229            MouseMoveEvent {
230                position,
231                modifiers,
232                pressed_button: button.into(),
233            },
234        );
235    }
236
237    /// Simulates a mouse down event.
238    pub fn simulate_mouse_down(
239        &mut self,
240        window: AnyWindowHandle,
241        position: Point<Pixels>,
242        button: MouseButton,
243        modifiers: Modifiers,
244    ) {
245        self.simulate_event(
246            window,
247            MouseDownEvent {
248                position,
249                modifiers,
250                button,
251                click_count: 1,
252                first_mouse: false,
253            },
254        );
255    }
256
257    /// Simulates a mouse up event.
258    pub fn simulate_mouse_up(
259        &mut self,
260        window: AnyWindowHandle,
261        position: Point<Pixels>,
262        button: MouseButton,
263        modifiers: Modifiers,
264    ) {
265        self.simulate_event(
266            window,
267            MouseUpEvent {
268                position,
269                modifiers,
270                button,
271                click_count: 1,
272            },
273        );
274    }
275
276    /// Simulates a click (mouse down followed by mouse up).
277    pub fn simulate_click(
278        &mut self,
279        window: AnyWindowHandle,
280        position: Point<Pixels>,
281        modifiers: Modifiers,
282    ) {
283        self.simulate_mouse_down(window, position, MouseButton::Left, modifiers);
284        self.simulate_mouse_up(window, position, MouseButton::Left, modifiers);
285    }
286
287    /// Simulates an input event on the given window.
288    pub fn simulate_event<E: InputEvent>(&mut self, window: AnyWindowHandle, event: E) {
289        self.update_window(window, |_, window, cx| {
290            window.dispatch_event(event.to_platform_input(), cx);
291        })
292        .ok();
293        self.run_until_parked();
294    }
295
296    /// Dispatches an action to the given window.
297    pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) {
298        self.update_window(window, |_, window, cx| {
299            window.dispatch_action(action.boxed_clone(), cx);
300        })
301        .ok();
302        self.run_until_parked();
303    }
304
305    /// Writes to the clipboard.
306    pub fn write_to_clipboard(&self, item: ClipboardItem) {
307        self.platform.write_to_clipboard(item);
308    }
309
310    /// Reads from the clipboard.
311    pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
312        self.platform.read_from_clipboard()
313    }
314
315    /// Waits for a condition to become true, with a timeout.
316    pub async fn wait_for<T: 'static>(
317        &mut self,
318        entity: &Entity<T>,
319        predicate: impl Fn(&T) -> bool,
320        timeout: Duration,
321    ) -> Result<()> {
322        let start = std::time::Instant::now();
323        loop {
324            {
325                let app = self.app.borrow();
326                if predicate(entity.read(&app)) {
327                    return Ok(());
328                }
329            }
330
331            if start.elapsed() > timeout {
332                return Err(anyhow!("Timed out waiting for condition"));
333            }
334
335            self.run_until_parked();
336            self.background_executor
337                .timer(Duration::from_millis(10))
338                .await;
339        }
340    }
341
342    /// Captures a screenshot of the specified window using direct texture capture.
343    ///
344    /// This renders the scene to a Metal texture and reads the pixels directly,
345    /// which does not require the window to be visible on screen.
346    #[cfg(any(test, feature = "test-support"))]
347    pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
348        self.update_window(window, |_, window, _cx| window.render_to_image())?
349    }
350
351    /// Waits for animations to complete by waiting a couple of frames.
352    pub async fn wait_for_animations(&self) {
353        self.background_executor
354            .timer(Duration::from_millis(32))
355            .await;
356        self.run_until_parked();
357    }
358}
359
360impl Default for VisualTestAppContext {
361    fn default() -> Self {
362        Self::new()
363    }
364}
365
366impl AppContext for VisualTestAppContext {
367    type Result<T> = T;
368
369    fn new<T: 'static>(
370        &mut self,
371        build_entity: impl FnOnce(&mut Context<T>) -> T,
372    ) -> Self::Result<Entity<T>> {
373        let mut app = self.app.borrow_mut();
374        app.new(build_entity)
375    }
376
377    fn reserve_entity<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
378        let mut app = self.app.borrow_mut();
379        app.reserve_entity()
380    }
381
382    fn insert_entity<T: 'static>(
383        &mut self,
384        reservation: crate::Reservation<T>,
385        build_entity: impl FnOnce(&mut Context<T>) -> T,
386    ) -> Self::Result<Entity<T>> {
387        let mut app = self.app.borrow_mut();
388        app.insert_entity(reservation, build_entity)
389    }
390
391    fn update_entity<T: 'static, R>(
392        &mut self,
393        handle: &Entity<T>,
394        update: impl FnOnce(&mut T, &mut Context<T>) -> R,
395    ) -> Self::Result<R> {
396        let mut app = self.app.borrow_mut();
397        app.update_entity(handle, update)
398    }
399
400    fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<crate::GpuiBorrow<'a, T>>
401    where
402        T: 'static,
403    {
404        panic!("Cannot use as_mut with a visual test app context. Try calling update() first")
405    }
406
407    fn read_entity<T, R>(
408        &self,
409        handle: &Entity<T>,
410        read: impl FnOnce(&T, &App) -> R,
411    ) -> Self::Result<R>
412    where
413        T: 'static,
414    {
415        let app = self.app.borrow();
416        app.read_entity(handle, read)
417    }
418
419    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
420    where
421        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
422    {
423        let mut lock = self.app.borrow_mut();
424        lock.update_window(window, f)
425    }
426
427    fn read_window<T, R>(
428        &self,
429        window: &WindowHandle<T>,
430        read: impl FnOnce(Entity<T>, &App) -> R,
431    ) -> Result<R>
432    where
433        T: 'static,
434    {
435        let app = self.app.borrow();
436        app.read_window(window, read)
437    }
438
439    fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
440    where
441        R: Send + 'static,
442    {
443        self.background_executor.spawn(future)
444    }
445
446    fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
447    where
448        G: Global,
449    {
450        let app = self.app.borrow();
451        callback(app.global::<G>(), &app)
452    }
453}