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