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