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}