visual_test_context.rs

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