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    /// Simulate answering a path selection dialog.
290    pub fn simulate_new_path_selection(
291        &self,
292        select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
293    ) {
294        self.platform.simulate_new_path_selection(select);
295    }
296
297    /// Check if a prompt dialog is pending.
298    pub fn has_pending_prompt(&self) -> bool {
299        self.platform.has_pending_prompt()
300    }
301
302    /// Simulate answering a prompt dialog.
303    pub fn simulate_prompt_answer(&self, button: &str) {
304        self.platform.simulate_prompt_answer(button);
305    }
306
307    /// Get all open windows.
308    pub fn windows(&self) -> Vec<AnyWindowHandle> {
309        self.read(|cx| cx.windows())
310    }
311}
312
313impl Default for TestApp {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319/// A test window with inspection and simulation capabilities.
320pub struct TestAppWindow<V> {
321    handle: WindowHandle<V>,
322    app: Rc<AppCell>,
323    platform: Rc<TestPlatform>,
324    background_executor: BackgroundExecutor,
325}
326
327impl<V: 'static + Render> TestAppWindow<V> {
328    /// Get the window handle.
329    pub fn handle(&self) -> WindowHandle<V> {
330        self.handle
331    }
332
333    /// Get the root view entity.
334    pub fn root(&self) -> Entity<V> {
335        let mut app = self.app.borrow_mut();
336        let any_handle: AnyWindowHandle = self.handle.into();
337        app.update_window(any_handle, |root_view, _, _| {
338            root_view.downcast::<V>().expect("root view type mismatch")
339        })
340        .expect("window not found")
341    }
342
343    /// Update the root view.
344    pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
345        let result = {
346            let mut app = self.app.borrow_mut();
347            let any_handle: AnyWindowHandle = self.handle.into();
348            app.update_window(any_handle, |root_view, window, cx| {
349                let view = root_view.downcast::<V>().expect("root view type mismatch");
350                view.update(cx, |view, cx| f(view, window, cx))
351            })
352            .expect("window not found")
353        };
354        self.background_executor.run_until_parked();
355        result
356    }
357
358    /// Read the root view.
359    pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
360        let app = self.app.borrow();
361        let view = self
362            .app
363            .borrow()
364            .windows
365            .get(self.handle.window_id())
366            .and_then(|w| w.as_ref())
367            .and_then(|w| w.root.clone())
368            .and_then(|r| r.downcast::<V>().ok())
369            .expect("window or root view not found");
370        f(view.read(&app), &app)
371    }
372
373    /// Get the window title.
374    pub fn title(&self) -> Option<String> {
375        let app = self.app.borrow();
376        app.read_window(&self.handle, |_, _cx| {
377            // TODO: expose title through Window API
378            None
379        })
380        .unwrap()
381    }
382
383    /// Simulate a keystroke.
384    pub fn simulate_keystroke(&mut self, keystroke: &str) {
385        let keystroke = Keystroke::parse(keystroke).unwrap();
386        {
387            let mut app = self.app.borrow_mut();
388            let any_handle: AnyWindowHandle = self.handle.into();
389            app.update_window(any_handle, |_, window, cx| {
390                window.dispatch_keystroke(keystroke, cx);
391            })
392            .unwrap();
393        }
394        self.background_executor.run_until_parked();
395    }
396
397    /// Simulate multiple keystrokes (space-separated).
398    pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
399        for keystroke in keystrokes.split(' ') {
400            self.simulate_keystroke(keystroke);
401        }
402    }
403
404    /// Simulate typing text.
405    pub fn simulate_input(&mut self, input: &str) {
406        for char in input.chars() {
407            self.simulate_keystroke(&char.to_string());
408        }
409    }
410
411    /// Simulate a mouse move.
412    pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
413        self.simulate_event(MouseMoveEvent {
414            position,
415            modifiers: Default::default(),
416            pressed_button: None,
417        });
418    }
419
420    /// Simulate a mouse down event.
421    pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
422        self.simulate_event(MouseDownEvent {
423            position,
424            button,
425            modifiers: Default::default(),
426            click_count: 1,
427            first_mouse: false,
428        });
429    }
430
431    /// Simulate a mouse up event.
432    pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
433        self.simulate_event(MouseUpEvent {
434            position,
435            button,
436            modifiers: Default::default(),
437            click_count: 1,
438        });
439    }
440
441    /// Simulate a click at the given position.
442    pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
443        self.simulate_mouse_down(position, button);
444        self.simulate_mouse_up(position, button);
445    }
446
447    /// Simulate a scroll event.
448    pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
449        self.simulate_event(crate::ScrollWheelEvent {
450            position,
451            delta: crate::ScrollDelta::Pixels(delta),
452            modifiers: Default::default(),
453            touch_phase: crate::TouchPhase::Moved,
454        });
455    }
456
457    /// Simulate an input event.
458    pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
459        let platform_input = event.to_platform_input();
460        {
461            let mut app = self.app.borrow_mut();
462            let any_handle: AnyWindowHandle = self.handle.into();
463            app.update_window(any_handle, |_, window, cx| {
464                window.dispatch_event(platform_input, cx);
465            })
466            .unwrap();
467        }
468        self.background_executor.run_until_parked();
469    }
470
471    /// Simulate resizing the window.
472    pub fn simulate_resize(&mut self, size: Size<Pixels>) {
473        let window_id = self.handle.window_id();
474        let mut app = self.app.borrow_mut();
475        if let Some(Some(window)) = app.windows.get_mut(window_id) {
476            if let Some(test_window) = window.platform_window.as_test() {
477                test_window.simulate_resize(size);
478            }
479        }
480        drop(app);
481        self.background_executor.run_until_parked();
482    }
483
484    /// Force a redraw of the window.
485    pub fn draw(&mut self) {
486        let mut app = self.app.borrow_mut();
487        let any_handle: AnyWindowHandle = self.handle.into();
488        app.update_window(any_handle, |_, window, cx| {
489            window.draw(cx).clear();
490        })
491        .unwrap();
492    }
493}
494
495impl<V> Clone for TestAppWindow<V> {
496    fn clone(&self) -> Self {
497        Self {
498            handle: self.handle,
499            app: self.app.clone(),
500            platform: self.platform.clone(),
501            background_executor: self.background_executor.clone(),
502        }
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use crate::{FocusHandle, Focusable, div, prelude::*};
510
511    struct Counter {
512        count: usize,
513        focus_handle: FocusHandle,
514    }
515
516    impl Counter {
517        fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
518            let focus_handle = cx.focus_handle();
519            Self {
520                count: 0,
521                focus_handle,
522            }
523        }
524
525        fn increment(&mut self, _cx: &mut Context<Self>) {
526            self.count += 1;
527        }
528    }
529
530    impl Focusable for Counter {
531        fn focus_handle(&self, _cx: &App) -> FocusHandle {
532            self.focus_handle.clone()
533        }
534    }
535
536    impl Render for Counter {
537        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
538            div().child(format!("Count: {}", self.count))
539        }
540    }
541
542    #[test]
543    fn test_basic_usage() {
544        let mut app = TestApp::new();
545
546        let mut window = app.open_window(Counter::new);
547
548        window.update(|counter, _window, cx| {
549            counter.increment(cx);
550        });
551
552        window.read(|counter, _| {
553            assert_eq!(counter.count, 1);
554        });
555
556        drop(window);
557        app.update(|cx| cx.shutdown());
558    }
559
560    #[test]
561    fn test_entity_creation() {
562        let mut app = TestApp::new();
563
564        let entity = app.new_entity(|cx| Counter {
565            count: 42,
566            focus_handle: cx.focus_handle(),
567        });
568
569        app.read_entity(&entity, |counter, _| {
570            assert_eq!(counter.count, 42);
571        });
572
573        app.update_entity(&entity, |counter, _cx| {
574            counter.count += 1;
575        });
576
577        app.read_entity(&entity, |counter, _| {
578            assert_eq!(counter.count, 43);
579        });
580    }
581
582    #[test]
583    fn test_globals() {
584        let mut app = TestApp::new();
585
586        struct MyGlobal(String);
587        impl Global for MyGlobal {}
588
589        assert!(!app.has_global::<MyGlobal>());
590
591        app.set_global(MyGlobal("hello".into()));
592
593        assert!(app.has_global::<MyGlobal>());
594
595        app.read_global::<MyGlobal, _>(|global, _| {
596            assert_eq!(global.0, "hello");
597        });
598
599        app.update_global::<MyGlobal, _>(|global, _| {
600            global.0 = "world".into();
601        });
602
603        app.read_global::<MyGlobal, _>(|global, _| {
604            assert_eq!(global.0, "world");
605        });
606    }
607}