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