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