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