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