test_app.rs

  1//! A clean testing API for GPUI applications.
  2//!
  3//! `TestApp` provides a simpler alternative to `TestAppContext` with:
  4//! - Automatic effect flushing after updates
  5//! - Clean window creation and inspection
  6//! - Input simulation helpers
  7//!
  8//! # Example
  9//! ```ignore
 10//! #[test]
 11//! fn test_my_view() {
 12//!     let mut app = TestApp::new();
 13//!
 14//!     let mut window = app.open_window(|window, cx| {
 15//!         MyView::new(window, cx)
 16//!     });
 17//!
 18//!     window.update(|view, window, cx| {
 19//!         view.do_something(cx);
 20//!     });
 21//!
 22//!     // Check rendered state
 23//!     assert_eq!(window.title(), Some("Expected Title"));
 24//! }
 25//! ```
 26
 27use crate::{
 28    AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,
 29    Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
 30    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
 31    SceneSnapshot, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds,
 32    WindowHandle, WindowOptions,
 33    app::GpuiMode,
 34};
 35use rand::{SeedableRng, rngs::StdRng};
 36use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
 37
 38/// A test application context with a clean API.
 39///
 40/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after
 41/// each update and provides simpler window management.
 42pub struct TestApp {
 43    app: Rc<AppCell>,
 44    platform: Rc<TestPlatform>,
 45    background_executor: BackgroundExecutor,
 46    foreground_executor: ForegroundExecutor,
 47    #[allow(dead_code)]
 48    dispatcher: TestDispatcher,
 49    text_system: Arc<TextSystem>,
 50}
 51
 52impl TestApp {
 53    /// Create a new test application.
 54    pub fn new() -> Self {
 55        Self::with_seed(0)
 56    }
 57
 58    /// Create a new test application with a specific random seed.
 59    pub fn with_seed(seed: u64) -> Self {
 60        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
 61        let arc_dispatcher = Arc::new(dispatcher.clone());
 62        let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
 63        let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
 64        let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
 65        let asset_source = Arc::new(());
 66        let http_client = http_client::FakeHttpClient::with_404_response();
 67        let text_system = Arc::new(TextSystem::new(platform.text_system()));
 68
 69        let mut app = App::new_app(platform.clone(), asset_source, http_client);
 70        app.borrow_mut().mode = GpuiMode::test();
 71
 72        Self {
 73            app,
 74            platform,
 75            background_executor,
 76            foreground_executor,
 77            dispatcher,
 78            text_system,
 79        }
 80    }
 81
 82    /// Run a closure with mutable access to the App context.
 83    /// Automatically runs until parked after the closure completes.
 84    pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
 85        let result = {
 86            let mut app = self.app.borrow_mut();
 87            app.update(f)
 88        };
 89        self.run_until_parked();
 90        result
 91    }
 92
 93    /// Run a closure with read-only access to the App context.
 94    pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
 95        let app = self.app.borrow();
 96        f(&app)
 97    }
 98
 99    /// Create a new entity in the app.
100    pub fn new_entity<T: 'static>(
101        &mut self,
102        build: impl FnOnce(&mut Context<T>) -> T,
103    ) -> Entity<T> {
104        self.update(|cx| cx.new(build))
105    }
106
107    /// Update an entity.
108    pub fn update_entity<T: 'static, R>(
109        &mut self,
110        entity: &Entity<T>,
111        f: impl FnOnce(&mut T, &mut Context<T>) -> R,
112    ) -> R {
113        self.update(|cx| entity.update(cx, f))
114    }
115
116    /// Read an entity.
117    pub fn read_entity<T: 'static, R>(&self, entity: &Entity<T>, f: impl FnOnce(&T, &App) -> R) -> R {
118        self.read(|cx| f(entity.read(cx), cx))
119    }
120
121    /// Open a test window with the given root view.
122    pub fn open_window<V: Render + 'static>(
123        &mut self,
124        build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
125    ) -> TestAppWindow<V> {
126        let bounds = self.read(|cx| Bounds::maximized(None, cx));
127        let handle = self.update(|cx| {
128            cx.open_window(
129                WindowOptions {
130                    window_bounds: Some(WindowBounds::Windowed(bounds)),
131                    ..Default::default()
132                },
133                |window, cx| cx.new(|cx| build_view(window, cx)),
134            )
135            .unwrap()
136        });
137
138        TestAppWindow {
139            handle,
140            app: self.app.clone(),
141            platform: self.platform.clone(),
142            background_executor: self.background_executor.clone(),
143        }
144    }
145
146    /// Open a test window with specific options.
147    pub fn open_window_with_options<V: Render + 'static>(
148        &mut self,
149        options: WindowOptions,
150        build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
151    ) -> TestAppWindow<V> {
152        let handle = self.update(|cx| {
153            cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
154                .unwrap()
155        });
156
157        TestAppWindow {
158            handle,
159            app: self.app.clone(),
160            platform: self.platform.clone(),
161            background_executor: self.background_executor.clone(),
162        }
163    }
164
165    /// Run pending tasks until there's nothing left to do.
166    pub fn run_until_parked(&self) {
167        self.background_executor.run_until_parked();
168    }
169
170    /// Advance the simulated clock by the given duration.
171    pub fn advance_clock(&self, duration: Duration) {
172        self.background_executor.advance_clock(duration);
173    }
174
175    /// Spawn a future on the foreground executor.
176    pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
177    where
178        Fut: Future<Output = R> + 'static,
179        R: 'static,
180    {
181        self.foreground_executor.spawn(f(self.to_async()))
182    }
183
184    /// Spawn a future on the background executor.
185    pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
186    where
187        R: Send + 'static,
188    {
189        self.background_executor.spawn(future)
190    }
191
192    /// Get an async handle to the app.
193    pub fn to_async(&self) -> AsyncApp {
194        AsyncApp {
195            app: Rc::downgrade(&self.app),
196            background_executor: self.background_executor.clone(),
197            foreground_executor: self.foreground_executor.clone(),
198        }
199    }
200
201    /// Get the background executor.
202    pub fn background_executor(&self) -> &BackgroundExecutor {
203        &self.background_executor
204    }
205
206    /// Get the foreground executor.
207    pub fn foreground_executor(&self) -> &ForegroundExecutor {
208        &self.foreground_executor
209    }
210
211    /// Get the text system.
212    pub fn text_system(&self) -> &Arc<TextSystem> {
213        &self.text_system
214    }
215
216    /// Check if a global of the given type exists.
217    pub fn has_global<G: Global>(&self) -> bool {
218        self.read(|cx| cx.has_global::<G>())
219    }
220
221    /// Set a global value.
222    pub fn set_global<G: Global>(&mut self, global: G) {
223        self.update(|cx| cx.set_global(global));
224    }
225
226    /// Read a global value.
227    pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
228        self.read(|cx| f(cx.global(), cx))
229    }
230
231    /// Update a global value.
232    pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
233        self.update(|cx| cx.update_global(f))
234    }
235
236    // Platform simulation methods
237
238    /// Write text to the simulated clipboard.
239    pub fn write_to_clipboard(&self, item: ClipboardItem) {
240        self.platform.write_to_clipboard(item);
241    }
242
243    /// Read from the simulated clipboard.
244    pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
245        self.platform.read_from_clipboard()
246    }
247
248    /// Get URLs that have been opened via `cx.open_url()`.
249    pub fn opened_url(&self) -> Option<String> {
250        self.platform.opened_url.borrow().clone()
251    }
252
253    /// Check if a file path prompt is pending.
254    pub fn did_prompt_for_new_path(&self) -> bool {
255        self.platform.did_prompt_for_new_path()
256    }
257
258    /// Simulate answering a path selection dialog.
259    pub fn simulate_new_path_selection(
260        &self,
261        select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
262    ) {
263        self.platform.simulate_new_path_selection(select);
264    }
265
266    /// Check if a prompt dialog is pending.
267    pub fn has_pending_prompt(&self) -> bool {
268        self.platform.has_pending_prompt()
269    }
270
271    /// Simulate answering a prompt dialog.
272    pub fn simulate_prompt_answer(&self, button: &str) {
273        self.platform.simulate_prompt_answer(button);
274    }
275
276    /// Get all open windows.
277    pub fn windows(&self) -> Vec<AnyWindowHandle> {
278        self.read(|cx| cx.windows())
279    }
280}
281
282impl Default for TestApp {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288/// A test window with inspection and simulation capabilities.
289pub struct TestAppWindow<V> {
290    handle: WindowHandle<V>,
291    app: Rc<AppCell>,
292    platform: Rc<TestPlatform>,
293    background_executor: BackgroundExecutor,
294}
295
296impl<V: 'static + Render> TestAppWindow<V> {
297    /// Get the window handle.
298    pub fn handle(&self) -> WindowHandle<V> {
299        self.handle
300    }
301
302    /// Get the root view entity.
303    pub fn root(&self) -> Entity<V> {
304        let mut app = self.app.borrow_mut();
305        let any_handle: AnyWindowHandle = self.handle.into();
306        app.update_window(any_handle, |root_view, _, _| {
307            root_view
308                .downcast::<V>()
309                .expect("root view type mismatch")
310        })
311        .expect("window not found")
312    }
313
314    /// Update the root view.
315    /// Automatically draws the window after the update to ensure the scene is current.
316    pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
317        let result = {
318            let mut app = self.app.borrow_mut();
319            let any_handle: AnyWindowHandle = self.handle.into();
320            app.update_window(any_handle, |root_view, window, cx| {
321                let view = root_view
322                    .downcast::<V>()
323                    .expect("root view type mismatch");
324                view.update(cx, |view, cx| f(view, window, cx))
325            })
326            .expect("window not found")
327        };
328        self.background_executor.run_until_parked();
329        self.draw();
330        result
331    }
332
333    /// Read the root view.
334    pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
335        let app = self.app.borrow();
336        let view = self
337            .app
338            .borrow()
339            .windows
340            .get(self.handle.window_id())
341            .and_then(|w| w.as_ref())
342            .and_then(|w| w.root.clone())
343            .and_then(|r| r.downcast::<V>().ok())
344            .expect("window or root view not found");
345        f(view.read(&app), &app)
346    }
347
348    /// Get the window title.
349    pub fn title(&self) -> Option<String> {
350        let app = self.app.borrow();
351        app.read_window(&self.handle, |_, _cx| {
352            // TODO: expose title through Window API
353            None
354        })
355        .unwrap()
356    }
357
358    /// Simulate a keystroke.
359    /// Automatically draws the window after the keystroke.
360    pub fn simulate_keystroke(&mut self, keystroke: &str) {
361        let keystroke = Keystroke::parse(keystroke).unwrap();
362        {
363            let mut app = self.app.borrow_mut();
364            let any_handle: AnyWindowHandle = self.handle.into();
365            app.update_window(any_handle, |_, window, cx| {
366                window.dispatch_keystroke(keystroke, cx);
367            })
368            .unwrap();
369        }
370        self.background_executor.run_until_parked();
371        self.draw();
372    }
373
374    /// Simulate multiple keystrokes (space-separated).
375    pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
376        for keystroke in keystrokes.split(' ') {
377            self.simulate_keystroke(keystroke);
378        }
379    }
380
381    /// Simulate typing text.
382    pub fn simulate_input(&mut self, input: &str) {
383        for char in input.chars() {
384            self.simulate_keystroke(&char.to_string());
385        }
386    }
387
388    /// Simulate a mouse move.
389    pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
390        self.simulate_event(MouseMoveEvent {
391            position,
392            modifiers: Default::default(),
393            pressed_button: None,
394        });
395    }
396
397    /// Simulate a mouse down event.
398    pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
399        self.simulate_event(MouseDownEvent {
400            position,
401            button,
402            modifiers: Default::default(),
403            click_count: 1,
404            first_mouse: false,
405        });
406    }
407
408    /// Simulate a mouse up event.
409    pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
410        self.simulate_event(MouseUpEvent {
411            position,
412            button,
413            modifiers: Default::default(),
414            click_count: 1,
415        });
416    }
417
418    /// Simulate a click at the given position.
419    pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
420        self.simulate_mouse_down(position, button);
421        self.simulate_mouse_up(position, button);
422    }
423
424    /// Simulate a scroll event.
425    pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
426        self.simulate_event(crate::ScrollWheelEvent {
427            position,
428            delta: crate::ScrollDelta::Pixels(delta),
429            modifiers: Default::default(),
430            touch_phase: crate::TouchPhase::Moved,
431        });
432    }
433
434    /// Simulate an input event.
435    /// Automatically draws the window after the event.
436    pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
437        let platform_input = event.to_platform_input();
438        {
439            let mut app = self.app.borrow_mut();
440            let any_handle: AnyWindowHandle = self.handle.into();
441            app.update_window(any_handle, |_, window, cx| {
442                window.dispatch_event(platform_input, cx);
443            })
444            .unwrap();
445        }
446        self.background_executor.run_until_parked();
447        self.draw();
448    }
449
450    /// Simulate resizing the window.
451    /// Automatically draws the window after the resize.
452    pub fn simulate_resize(&mut self, size: Size<Pixels>) {
453        let window_id = self.handle.window_id();
454        let mut app = self.app.borrow_mut();
455        if let Some(Some(window)) = app.windows.get_mut(window_id) {
456            if let Some(test_window) = window.platform_window.as_test() {
457                test_window.simulate_resize(size);
458            }
459        }
460        drop(app);
461        self.background_executor.run_until_parked();
462        self.draw();
463    }
464
465    /// Force a redraw of the window.
466    pub fn draw(&mut self) {
467        let mut app = self.app.borrow_mut();
468        let any_handle: AnyWindowHandle = self.handle.into();
469        app.update_window(any_handle, |_, window, cx| {
470            window.draw(cx).clear();
471        })
472        .unwrap();
473    }
474
475    /// Get a snapshot of the rendered scene for inspection.
476    /// The scene is automatically kept up to date after `update()` and `simulate_*()` calls.
477    pub fn scene_snapshot(&self) -> SceneSnapshot {
478        let app = self.app.borrow();
479        let window = app
480            .windows
481            .get(self.handle.window_id())
482            .and_then(|w| w.as_ref())
483            .expect("window not found");
484        window.rendered_frame.scene.snapshot()
485    }
486}
487
488impl<V> Clone for TestAppWindow<V> {
489    fn clone(&self) -> Self {
490        Self {
491            handle: self.handle,
492            app: self.app.clone(),
493            platform: self.platform.clone(),
494            background_executor: self.background_executor.clone(),
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use crate::{div, prelude::*, Focusable, FocusHandle};
503
504    struct Counter {
505        count: usize,
506        focus_handle: FocusHandle,
507    }
508
509    impl Counter {
510        fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
511            let focus_handle = cx.focus_handle();
512            Self {
513                count: 0,
514                focus_handle,
515            }
516        }
517
518        fn increment(&mut self, _cx: &mut Context<Self>) {
519            self.count += 1;
520        }
521    }
522
523    impl Focusable for Counter {
524        fn focus_handle(&self, _cx: &App) -> FocusHandle {
525            self.focus_handle.clone()
526        }
527    }
528
529    impl Render for Counter {
530        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
531            div().child(format!("Count: {}", self.count))
532        }
533    }
534
535    #[test]
536    fn test_basic_usage() {
537        let mut app = TestApp::new();
538
539        let mut window = app.open_window(Counter::new);
540
541        window.update(|counter, _window, cx| {
542            counter.increment(cx);
543        });
544
545        window.read(|counter, _| {
546            assert_eq!(counter.count, 1);
547        });
548    }
549
550    #[test]
551    fn test_entity_creation() {
552        let mut app = TestApp::new();
553
554        let entity = app.new_entity(|cx| Counter {
555            count: 42,
556            focus_handle: cx.focus_handle(),
557        });
558
559        app.read_entity(&entity, |counter, _| {
560            assert_eq!(counter.count, 42);
561        });
562
563        app.update_entity(&entity, |counter, _cx| {
564            counter.count += 1;
565        });
566
567        app.read_entity(&entity, |counter, _| {
568            assert_eq!(counter.count, 43);
569        });
570    }
571
572    #[test]
573    fn test_globals() {
574        let mut app = TestApp::new();
575
576        struct MyGlobal(String);
577        impl Global for MyGlobal {}
578
579        assert!(!app.has_global::<MyGlobal>());
580
581        app.set_global(MyGlobal("hello".into()));
582
583        assert!(app.has_global::<MyGlobal>());
584
585        app.read_global::<MyGlobal, _>(|global, _| {
586            assert_eq!(global.0, "hello");
587        });
588
589        app.update_global::<MyGlobal, _>(|global, _| {
590            global.0 = "world".into();
591        });
592
593        app.read_global::<MyGlobal, _>(|global, _| {
594            assert_eq!(global.0, "world");
595        });
596    }
597}