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}