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, app::GpuiMode,
33};
34use rand::{SeedableRng, rngs::StdRng};
35use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
36
37/// A test application context with a clean API.
38///
39/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after
40/// each update and provides simpler window management.
41pub struct TestApp {
42 app: Rc<AppCell>,
43 platform: Rc<TestPlatform>,
44 background_executor: BackgroundExecutor,
45 foreground_executor: ForegroundExecutor,
46 #[allow(dead_code)]
47 dispatcher: TestDispatcher,
48 text_system: Arc<TextSystem>,
49}
50
51impl TestApp {
52 /// Create a new test application.
53 pub fn new() -> Self {
54 Self::with_seed(0)
55 }
56
57 /// Create a new test application with a specific random seed.
58 pub fn with_seed(seed: u64) -> Self {
59 let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
60 let arc_dispatcher = Arc::new(dispatcher.clone());
61 let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
62 let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
63 let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
64 let asset_source = Arc::new(());
65 let http_client = http_client::FakeHttpClient::with_404_response();
66 let text_system = Arc::new(TextSystem::new(platform.text_system()));
67
68 let mut app = App::new_app(platform.clone(), asset_source, http_client);
69 app.borrow_mut().mode = GpuiMode::test();
70
71 Self {
72 app,
73 platform,
74 background_executor,
75 foreground_executor,
76 dispatcher,
77 text_system,
78 }
79 }
80
81 /// Run a closure with mutable access to the App context.
82 /// Automatically runs until parked after the closure completes.
83 pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
84 let result = {
85 let mut app = self.app.borrow_mut();
86 app.update(f)
87 };
88 self.run_until_parked();
89 result
90 }
91
92 /// Run a closure with read-only access to the App context.
93 pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
94 let app = self.app.borrow();
95 f(&app)
96 }
97
98 /// Create a new entity in the app.
99 pub fn new_entity<T: 'static>(
100 &mut self,
101 build: impl FnOnce(&mut Context<T>) -> T,
102 ) -> Entity<T> {
103 self.update(|cx| cx.new(build))
104 }
105
106 /// Update an entity.
107 pub fn update_entity<T: 'static, R>(
108 &mut self,
109 entity: &Entity<T>,
110 f: impl FnOnce(&mut T, &mut Context<T>) -> R,
111 ) -> R {
112 self.update(|cx| entity.update(cx, f))
113 }
114
115 /// Read an entity.
116 pub fn read_entity<T: 'static, R>(
117 &self,
118 entity: &Entity<T>,
119 f: impl FnOnce(&T, &App) -> R,
120 ) -> R {
121 self.read(|cx| f(entity.read(cx), cx))
122 }
123
124 /// Open a test window with the given root view.
125 pub fn open_window<V: Render + 'static>(
126 &mut self,
127 build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
128 ) -> TestWindow<V> {
129 let bounds = self.read(|cx| Bounds::maximized(None, cx));
130 let handle = self.update(|cx| {
131 cx.open_window(
132 WindowOptions {
133 window_bounds: Some(WindowBounds::Windowed(bounds)),
134 ..Default::default()
135 },
136 |window, cx| cx.new(|cx| build_view(window, cx)),
137 )
138 .unwrap()
139 });
140
141 TestWindow {
142 handle,
143 app: self.app.clone(),
144 platform: self.platform.clone(),
145 background_executor: self.background_executor.clone(),
146 }
147 }
148
149 /// Open a test window with specific options.
150 pub fn open_window_with_options<V: Render + 'static>(
151 &mut self,
152 options: WindowOptions,
153 build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
154 ) -> TestWindow<V> {
155 let handle = self.update(|cx| {
156 cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
157 .unwrap()
158 });
159
160 TestWindow {
161 handle,
162 app: self.app.clone(),
163 platform: self.platform.clone(),
164 background_executor: self.background_executor.clone(),
165 }
166 }
167
168 /// Run pending tasks until there's nothing left to do.
169 pub fn run_until_parked(&self) {
170 self.background_executor.run_until_parked();
171 }
172
173 /// Advance the simulated clock by the given duration.
174 pub fn advance_clock(&self, duration: Duration) {
175 self.background_executor.advance_clock(duration);
176 }
177
178 /// Spawn a future on the foreground executor.
179 pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
180 where
181 Fut: Future<Output = R> + 'static,
182 R: 'static,
183 {
184 self.foreground_executor.spawn(f(self.to_async()))
185 }
186
187 /// Spawn a future on the background executor.
188 pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
189 where
190 R: Send + 'static,
191 {
192 self.background_executor.spawn(future)
193 }
194
195 /// Get an async handle to the app.
196 pub fn to_async(&self) -> AsyncApp {
197 AsyncApp {
198 app: Rc::downgrade(&self.app),
199 background_executor: self.background_executor.clone(),
200 foreground_executor: self.foreground_executor.clone(),
201 }
202 }
203
204 /// Get the background executor.
205 pub fn background_executor(&self) -> &BackgroundExecutor {
206 &self.background_executor
207 }
208
209 /// Get the foreground executor.
210 pub fn foreground_executor(&self) -> &ForegroundExecutor {
211 &self.foreground_executor
212 }
213
214 /// Get the text system.
215 pub fn text_system(&self) -> &Arc<TextSystem> {
216 &self.text_system
217 }
218
219 /// Check if a global of the given type exists.
220 pub fn has_global<G: Global>(&self) -> bool {
221 self.read(|cx| cx.has_global::<G>())
222 }
223
224 /// Set a global value.
225 pub fn set_global<G: Global>(&mut self, global: G) {
226 self.update(|cx| cx.set_global(global));
227 }
228
229 /// Read a global value.
230 pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
231 self.read(|cx| f(cx.global(), cx))
232 }
233
234 /// Update a global value.
235 pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
236 self.update(|cx| cx.update_global(f))
237 }
238
239 // Platform simulation methods
240
241 /// Write text to the simulated clipboard.
242 pub fn write_to_clipboard(&self, item: ClipboardItem) {
243 self.platform.write_to_clipboard(item);
244 }
245
246 /// Read from the simulated clipboard.
247 pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
248 self.platform.read_from_clipboard()
249 }
250
251 /// Get URLs that have been opened via `cx.open_url()`.
252 pub fn opened_url(&self) -> Option<String> {
253 self.platform.opened_url.borrow().clone()
254 }
255
256 /// Check if a file path prompt is pending.
257 pub fn did_prompt_for_new_path(&self) -> bool {
258 self.platform.did_prompt_for_new_path()
259 }
260
261 /// Simulate answering a path selection dialog.
262 pub fn simulate_new_path_selection(
263 &self,
264 select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
265 ) {
266 self.platform.simulate_new_path_selection(select);
267 }
268
269 /// Check if a prompt dialog is pending.
270 pub fn has_pending_prompt(&self) -> bool {
271 self.platform.has_pending_prompt()
272 }
273
274 /// Simulate answering a prompt dialog.
275 pub fn simulate_prompt_answer(&self, button: &str) {
276 self.platform.simulate_prompt_answer(button);
277 }
278
279 /// Get all open windows.
280 pub fn windows(&self) -> Vec<AnyWindowHandle> {
281 self.read(|cx| cx.windows())
282 }
283}
284
285impl Default for TestApp {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291/// A test window with inspection and simulation capabilities.
292pub struct TestWindow<V> {
293 handle: WindowHandle<V>,
294 app: Rc<AppCell>,
295 platform: Rc<TestPlatform>,
296 background_executor: BackgroundExecutor,
297}
298
299impl<V: 'static + Render> TestWindow<V> {
300 /// Get the window handle.
301 pub fn handle(&self) -> WindowHandle<V> {
302 self.handle
303 }
304
305 /// Get the root view entity.
306 pub fn root(&self) -> Entity<V> {
307 let mut app = self.app.borrow_mut();
308 let any_handle: AnyWindowHandle = self.handle.into();
309 app.update_window(any_handle, |root_view, _, _| {
310 root_view.downcast::<V>().expect("root view type mismatch")
311 })
312 .expect("window not found")
313 }
314
315 /// Update the root view.
316 /// Automatically draws the window after the update to ensure the scene is current.
317 pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
318 let result = {
319 let mut app = self.app.borrow_mut();
320 let any_handle: AnyWindowHandle = self.handle.into();
321 app.update_window(any_handle, |root_view, window, cx| {
322 let view = root_view.downcast::<V>().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 self.draw();
329 result
330 }
331
332 /// Read the root view.
333 pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
334 let app = self.app.borrow();
335 let view = self
336 .app
337 .borrow()
338 .windows
339 .get(self.handle.window_id())
340 .and_then(|w| w.as_ref())
341 .and_then(|w| w.root.clone())
342 .and_then(|r| r.downcast::<V>().ok())
343 .expect("window or root view not found");
344 f(view.read(&app), &app)
345 }
346
347 /// Get the window title.
348 pub fn title(&self) -> Option<String> {
349 let app = self.app.borrow();
350 app.read_window(&self.handle, |_, _cx| {
351 // TODO: expose title through Window API
352 None
353 })
354 .unwrap()
355 }
356
357 /// Simulate a keystroke.
358 /// Automatically draws the window after the keystroke.
359 pub fn simulate_keystroke(&mut self, keystroke: &str) {
360 let keystroke = Keystroke::parse(keystroke).unwrap();
361 {
362 let mut app = self.app.borrow_mut();
363 let any_handle: AnyWindowHandle = self.handle.into();
364 app.update_window(any_handle, |_, window, cx| {
365 window.dispatch_keystroke(keystroke, cx);
366 })
367 .unwrap();
368 }
369 self.background_executor.run_until_parked();
370 self.draw();
371 }
372
373 /// Simulate multiple keystrokes (space-separated).
374 pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
375 for keystroke in keystrokes.split(' ') {
376 self.simulate_keystroke(keystroke);
377 }
378 }
379
380 /// Simulate typing text.
381 pub fn simulate_input(&mut self, input: &str) {
382 for char in input.chars() {
383 self.simulate_keystroke(&char.to_string());
384 }
385 }
386
387 /// Simulate a mouse move.
388 pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
389 self.simulate_event(MouseMoveEvent {
390 position,
391 modifiers: Default::default(),
392 pressed_button: None,
393 });
394 }
395
396 /// Simulate a mouse down event.
397 pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
398 self.simulate_event(MouseDownEvent {
399 position,
400 button,
401 modifiers: Default::default(),
402 click_count: 1,
403 first_mouse: false,
404 });
405 }
406
407 /// Simulate a mouse up event.
408 pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
409 self.simulate_event(MouseUpEvent {
410 position,
411 button,
412 modifiers: Default::default(),
413 click_count: 1,
414 });
415 }
416
417 /// Simulate a click at the given position.
418 pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
419 self.simulate_mouse_down(position, button);
420 self.simulate_mouse_up(position, button);
421 }
422
423 /// Simulate a scroll event.
424 pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
425 self.simulate_event(crate::ScrollWheelEvent {
426 position,
427 delta: crate::ScrollDelta::Pixels(delta),
428 modifiers: Default::default(),
429 touch_phase: crate::TouchPhase::Moved,
430 });
431 }
432
433 /// Simulate an input event.
434 /// Automatically draws the window after the event.
435 pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
436 let platform_input = event.to_platform_input();
437 {
438 let mut app = self.app.borrow_mut();
439 let any_handle: AnyWindowHandle = self.handle.into();
440 app.update_window(any_handle, |_, window, cx| {
441 window.dispatch_event(platform_input, cx);
442 })
443 .unwrap();
444 }
445 self.background_executor.run_until_parked();
446 self.draw();
447 }
448
449 /// Simulate resizing the window.
450 /// Automatically draws the window after the resize.
451 pub fn simulate_resize(&mut self, size: Size<Pixels>) {
452 let window_id = self.handle.window_id();
453 let mut app = self.app.borrow_mut();
454 if let Some(Some(window)) = app.windows.get_mut(window_id) {
455 if let Some(test_window) = window.platform_window.as_test() {
456 test_window.simulate_resize(size);
457 }
458 }
459 drop(app);
460 self.background_executor.run_until_parked();
461 self.draw();
462 }
463
464 /// Force a redraw of the window.
465 pub fn draw(&mut self) {
466 let mut app = self.app.borrow_mut();
467 let any_handle: AnyWindowHandle = self.handle.into();
468 app.update_window(any_handle, |_, window, cx| {
469 window.draw(cx).clear();
470 })
471 .unwrap();
472 }
473
474 /// Get a snapshot of the rendered scene for inspection.
475 /// The scene is automatically kept up to date after `update()` and `simulate_*()` calls.
476 pub fn scene_snapshot(&self) -> SceneSnapshot {
477 let app = self.app.borrow();
478 let window = app
479 .windows
480 .get(self.handle.window_id())
481 .and_then(|w| w.as_ref())
482 .expect("window not found");
483 window.rendered_frame.scene.snapshot()
484 }
485
486 /// Get the named diagnostic quads recorded during imperative paint, without inspecting the
487 /// rest of the scene snapshot.
488 ///
489 /// This is useful for tests that want a stable, semantic view of layout/paint geometry without
490 /// coupling to the low-level quad/glyph output.
491 pub fn diagnostic_quads(&self) -> Vec<crate::scene::test_scene::DiagnosticQuad> {
492 self.scene_snapshot().diagnostic_quads
493 }
494}
495
496impl<V> Clone for TestWindow<V> {
497 fn clone(&self) -> Self {
498 Self {
499 handle: self.handle,
500 app: self.app.clone(),
501 platform: self.platform.clone(),
502 background_executor: self.background_executor.clone(),
503 }
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::{FocusHandle, Focusable, div, prelude::*};
511
512 struct Counter {
513 count: usize,
514 focus_handle: FocusHandle,
515 }
516
517 impl Counter {
518 fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
519 let focus_handle = cx.focus_handle();
520 Self {
521 count: 0,
522 focus_handle,
523 }
524 }
525
526 fn increment(&mut self, _cx: &mut Context<Self>) {
527 self.count += 1;
528 }
529 }
530
531 impl Focusable for Counter {
532 fn focus_handle(&self, _cx: &App) -> FocusHandle {
533 self.focus_handle.clone()
534 }
535 }
536
537 impl Render for Counter {
538 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
539 div().child(format!("Count: {}", self.count))
540 }
541 }
542
543 #[test]
544 fn test_basic_usage() {
545 let mut app = TestApp::new();
546
547 let mut window = app.open_window(Counter::new);
548
549 window.update(|counter, _window, cx| {
550 counter.increment(cx);
551 });
552
553 window.read(|counter, _| {
554 assert_eq!(counter.count, 1);
555 });
556 }
557
558 #[test]
559 fn test_entity_creation() {
560 let mut app = TestApp::new();
561
562 let entity = app.new_entity(|cx| Counter {
563 count: 42,
564 focus_handle: cx.focus_handle(),
565 });
566
567 app.read_entity(&entity, |counter, _| {
568 assert_eq!(counter.count, 42);
569 });
570
571 app.update_entity(&entity, |counter, _cx| {
572 counter.count += 1;
573 });
574
575 app.read_entity(&entity, |counter, _| {
576 assert_eq!(counter.count, 43);
577 });
578 }
579
580 #[test]
581 fn test_globals() {
582 let mut app = TestApp::new();
583
584 struct MyGlobal(String);
585 impl Global for MyGlobal {}
586
587 assert!(!app.has_global::<MyGlobal>());
588
589 app.set_global(MyGlobal("hello".into()));
590
591 assert!(app.has_global::<MyGlobal>());
592
593 app.read_global::<MyGlobal, _>(|global, _| {
594 assert_eq!(global.0, "hello");
595 });
596
597 app.update_global::<MyGlobal, _>(|global, _| {
598 global.0 = "world".into();
599 });
600
601 app.read_global::<MyGlobal, _>(|global, _| {
602 assert_eq!(global.0, "world");
603 });
604 }
605}