gpui/app/test_app.rs
1//! A headless app for tests of views, entities and input.
2//!
3//! [`TestApp`] runs a real [`App`] on a deterministic scheduler, with a test
4//! platform in place of the operating system. Open windows with
5//! [`TestApp::open_window`], send them input with the `simulate_*` methods on
6//! [`TestAppWindow`], and read the results back. Tasks spawned in the app don't
7//! run until you call [`TestApp::run_until_parked`] or an `update` method.
8//!
9//! Use [`TestAppContext`](crate::TestAppContext) instead when a test needs an
10//! async function, several apps or randomized scheduling. The
11//! [testing guide](crate::_documentation::guides::testing) covers both.
12//!
13//! # Example
14//!
15//! ```
16//! use gpui::{Context, MouseButton, TestApp, Window, div, point, prelude::*, px};
17//!
18//! struct Counter {
19//! count: usize,
20//! }
21//!
22//! impl Render for Counter {
23//! fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
24//! div()
25//! .id("increment")
26//! .size(px(100.))
27//! .on_click(cx.listener(|this, _event, _window, cx| {
28//! this.count += 1;
29//! cx.notify();
30//! }))
31//! .child(format!("{}", self.count))
32//! }
33//! }
34//!
35//! let mut app = TestApp::new();
36//! let mut window = app.open_window(|_, _| Counter { count: 0 });
37//!
38//! // Paint once, so the click has bounds to hit.
39//! window.draw();
40//! window.simulate_click(point(px(10.), px(10.)), MouseButton::Left);
41//!
42//! window.read(|counter, _| assert_eq!(counter.count, 1));
43//! ```
44
45use crate::{
46 AnyWindowHandle, App, AppCell, AppContext, AssetRegistry, AsyncApp, BackgroundExecutor,
47 BorrowAppContext, Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global,
48 InputEvent, Keystroke, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
49 Platform, PlatformTextSystem, Point, Render, Size, Task, TestDispatcher, TestPlatform,
50 TextSystem, Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
51};
52use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
53
54/// A headless app for tests.
55///
56/// `TestApp` runs a real [`App`] on a deterministic scheduler. Its platform keeps
57/// the clipboard, opened URLs and prompts in memory, so you can assert on them
58/// instead of seeing them on screen. Each `TestApp` is independent: state set in
59/// one doesn't appear in another.
60///
61/// See the [testing guide](crate::_documentation::guides::testing).
62pub struct TestApp {
63 app: Rc<AppCell>,
64 platform: Rc<TestPlatform>,
65 background_executor: BackgroundExecutor,
66 foreground_executor: ForegroundExecutor,
67 #[allow(dead_code)]
68 dispatcher: TestDispatcher,
69 text_system: Arc<TextSystem>,
70}
71
72impl TestApp {
73 /// Creates a test app with scheduler seed `0`.
74 ///
75 /// Text is shaped by the test text system, not by real fonts. Use
76 /// [`TestApp::with_text_system`] when a test measures real glyphs.
77 pub fn new() -> Self {
78 Self::with_seed(0)
79 }
80
81 /// Creates a test app whose scheduler orders ready tasks by `seed`.
82 ///
83 /// The seed decides which ready task runs first. The same seed gives the same
84 /// order every time, so a failing seed can be replayed.
85 pub fn with_seed(seed: u64) -> Self {
86 Self::build(seed, None, Arc::new(()))
87 }
88
89 /// Creates a test app that shapes text with `text_system`. Uses seed `0`.
90 ///
91 /// Pass a real text system to measure glyphs as the app would on a platform.
92 pub fn with_text_system(text_system: Arc<dyn PlatformTextSystem>) -> Self {
93 Self::build(0, Some(text_system), Arc::new(()))
94 }
95
96 /// Creates a test app with the given text system and asset source.
97 ///
98 /// This is [`TestApp::with_text_system`] plus an [`AssetSource`](crate::AssetSource),
99 /// so views can load icons and other assets. Uses seed `0`.
100 pub fn with_text_system_and_assets(
101 text_system: Arc<dyn PlatformTextSystem>,
102 asset_source: Arc<dyn crate::AssetSource>,
103 ) -> Self {
104 Self::build(0, Some(text_system), asset_source)
105 }
106
107 fn build(
108 seed: u64,
109 platform_text_system: Option<Arc<dyn PlatformTextSystem>>,
110 asset_source: Arc<dyn crate::AssetSource>,
111 ) -> Self {
112 let dispatcher = TestDispatcher::new(seed);
113 let arc_dispatcher = Arc::new(dispatcher.clone());
114 let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
115 let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
116 let platform = match platform_text_system.clone() {
117 Some(ts) => TestPlatform::with_text_system(
118 background_executor.clone(),
119 foreground_executor.clone(),
120 ts,
121 ),
122 None => TestPlatform::new(background_executor.clone(), foreground_executor.clone()),
123 };
124 let http_client = crate::http_client::FakeHttpClient::with_404_response();
125 let text_system = Arc::new(TextSystem::new(
126 platform_text_system.unwrap_or_else(|| platform.text_system.clone()),
127 ));
128
129 let app = App::new_app(
130 platform.clone(),
131 AssetRegistry::from(asset_source).into(),
132 http_client,
133 );
134 app.borrow_mut().mode = GpuiMode::test();
135
136 Self {
137 app,
138 platform,
139 background_executor,
140 foreground_executor,
141 dispatcher,
142 text_system,
143 }
144 }
145
146 /// Runs `f` with mutable access to the [`App`], then runs pending tasks until none can make progress.
147 ///
148 /// Effects such as observers and subscriptions run when `f` returns, before the
149 /// pending tasks run. Timers that haven't expired stay pending; use
150 /// [`TestApp::advance_clock`] to fire them.
151 ///
152 /// # Example
153 ///
154 /// ```
155 /// # use gpui::TestApp;
156 /// # use std::{cell::Cell, rc::Rc};
157 /// let mut app = TestApp::new();
158 /// let finished = Rc::new(Cell::new(false));
159 ///
160 /// let flag = finished.clone();
161 /// app.spawn(|_| async move { flag.set(true) }).detach();
162 ///
163 /// // Any update runs the task that was spawned above.
164 /// app.update(|_| ());
165 /// assert!(finished.get());
166 /// ```
167 pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
168 let result = {
169 let mut app = self.app.borrow_mut();
170 app.update(f)
171 };
172 self.run_until_parked();
173 result
174 }
175
176 /// Runs `f` with read-only access to the [`App`].
177 ///
178 /// This doesn't run pending tasks. A task that is still waiting to run doesn't
179 /// run before `f` is called.
180 pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
181 let app = self.app.borrow();
182 f(&app)
183 }
184
185 /// Creates an entity from `build` and runs pending tasks until parked.
186 ///
187 /// This is `cx.new(build)` inside [`TestApp::update`].
188 pub fn new_entity<T: 'static>(
189 &mut self,
190 build: impl FnOnce(&mut Context<T>) -> T,
191 ) -> Entity<T> {
192 self.update(|cx| cx.new(build))
193 }
194
195 /// Updates an entity and runs pending tasks until parked.
196 pub fn update_entity<T: 'static, R>(
197 &mut self,
198 entity: &Entity<T>,
199 f: impl FnOnce(&mut T, &mut Context<T>) -> R,
200 ) -> R {
201 self.update(|cx| entity.update(cx, f))
202 }
203
204 /// Reads an entity without running pending tasks.
205 pub fn read_entity<T: 'static, R>(
206 &self,
207 entity: &Entity<T>,
208 f: impl FnOnce(&T, &App) -> R,
209 ) -> R {
210 self.read(|cx| f(entity.read(cx), cx))
211 }
212
213 /// Opens a window sized to the test display and returns a [`TestAppWindow`] for its root view.
214 ///
215 /// The window's bounds are the maximized bounds of the test display. Use
216 /// [`TestApp::open_window_with_options`] to choose other bounds.
217 ///
218 /// # Panics
219 ///
220 /// Panics if the window can't be opened.
221 pub fn open_window<V: Render + 'static>(
222 &mut self,
223 build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
224 ) -> TestAppWindow<V> {
225 let bounds = self.read(|cx| Bounds::maximized(None, cx));
226 let handle = self.update(|cx| {
227 cx.open_window(
228 WindowOptions::new().window_bounds(Some(WindowBounds::Windowed(bounds))),
229 |window, cx| cx.new(|cx| build_view(window, cx)),
230 )
231 .unwrap()
232 });
233
234 TestAppWindow {
235 handle,
236 app: self.app.clone(),
237 platform: self.platform.clone(),
238 background_executor: self.background_executor.clone(),
239 }
240 }
241
242 /// Opens a window with `options` and returns a [`TestAppWindow`] for its root view.
243 ///
244 /// # Panics
245 ///
246 /// Panics if the window can't be opened.
247 pub fn open_window_with_options<V: Render + 'static>(
248 &mut self,
249 options: WindowOptions,
250 build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
251 ) -> TestAppWindow<V> {
252 let handle = self.update(|cx| {
253 cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
254 .unwrap()
255 });
256
257 TestAppWindow {
258 handle,
259 app: self.app.clone(),
260 platform: self.platform.clone(),
261 background_executor: self.background_executor.clone(),
262 }
263 }
264
265 /// Runs tasks until none can make progress.
266 ///
267 /// This doesn't move the simulated clock. A timer that hasn't expired stays
268 /// pending until [`TestApp::advance_clock`] moves time past it.
269 ///
270 /// # Example
271 ///
272 /// ```
273 /// # use gpui::TestApp;
274 /// # use std::{cell::Cell, rc::Rc, time::Duration};
275 /// let mut app = TestApp::new();
276 /// let fired = Rc::new(Cell::new(false));
277 ///
278 /// let flag = fired.clone();
279 /// app.spawn(|cx| async move {
280 /// cx.background_executor().timer(Duration::from_secs(1)).await;
281 /// flag.set(true);
282 /// })
283 /// .detach();
284 ///
285 /// app.run_until_parked();
286 /// assert!(!fired.get(), "the timer hasn't expired yet");
287 ///
288 /// app.advance_clock(Duration::from_secs(1));
289 /// app.run_until_parked();
290 /// assert!(fired.get());
291 /// ```
292 pub fn run_until_parked(&self) {
293 self.background_executor.run_until_parked();
294 }
295
296 /// Moves the simulated clock forward by `duration`.
297 ///
298 /// Tasks that become ready run during the call, including timers that expire
299 /// within the window. Use this to test timeouts and delays, as in the example for
300 /// [`TestApp::run_until_parked`].
301 pub fn advance_clock(&self, duration: Duration) {
302 self.background_executor.advance_clock(duration);
303 }
304
305 /// Spawns a future on the foreground executor, given an [`AsyncApp`] for this app.
306 ///
307 /// The future runs when the app runs tasks: during [`TestApp::run_until_parked`],
308 /// [`TestApp::advance_clock`] or the next `update`. Await, store or detach the
309 /// returned [`Task`], as in a real app. See [async work](crate::_documentation::guides::async_work).
310 pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
311 where
312 Fut: Future<Output = R> + 'static,
313 R: 'static,
314 {
315 self.foreground_executor.spawn(f(self.to_async()))
316 }
317
318 /// Spawns a `Send` future on the background executor.
319 ///
320 /// Like [`TestApp::spawn`], the future runs when the app runs tasks.
321 pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
322 where
323 R: Send + 'static,
324 {
325 self.background_executor.spawn(future)
326 }
327
328 /// Returns an [`AsyncApp`] for this app.
329 ///
330 /// The handle holds a weak reference, so it can't keep the app alive.
331 pub fn to_async(&self) -> AsyncApp {
332 AsyncApp {
333 app: Rc::downgrade(&self.app),
334 background_executor: self.background_executor.clone(),
335 foreground_executor: self.foreground_executor.clone(),
336 }
337 }
338
339 /// Returns the test background executor.
340 pub fn background_executor(&self) -> &BackgroundExecutor {
341 &self.background_executor
342 }
343
344 /// Returns the test foreground executor.
345 pub fn foreground_executor(&self) -> &ForegroundExecutor {
346 &self.foreground_executor
347 }
348
349 /// Returns the text system the app uses to shape and measure text.
350 pub fn text_system(&self) -> &Arc<TextSystem> {
351 &self.text_system
352 }
353
354 /// Returns whether a global of type `G` is set.
355 pub fn has_global<G: Global>(&self) -> bool {
356 self.read(|cx| cx.has_global::<G>())
357 }
358
359 /// Sets the global of type `G`, replacing any previous value, then runs pending tasks.
360 pub fn set_global<G: Global>(&mut self, global: G) {
361 self.update(|cx| cx.set_global(global));
362 }
363
364 /// Runs `f` with the global of type `G`.
365 ///
366 /// # Panics
367 ///
368 /// Panics if no global of type `G` is set. Check with [`TestApp::has_global`] first.
369 pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
370 self.read(|cx| f(cx.global(), cx))
371 }
372
373 /// Runs `f` with mutable access to the global of type `G`, then runs pending tasks.
374 ///
375 /// # Panics
376 ///
377 /// Panics if no global of type `G` is set.
378 pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
379 self.update(|cx| cx.update_global(f))
380 }
381
382 // Platform simulation methods
383
384 /// Writes `item` to the test clipboard.
385 ///
386 /// [`TestApp::read_from_clipboard`] returns the item until something else is written.
387 pub fn write_to_clipboard(&self, item: ClipboardItem) {
388 self.platform.write_to_clipboard(item);
389 }
390
391 /// Returns the item most recently written to the test clipboard, or `None` if nothing was written.
392 pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
393 self.platform.read_from_clipboard()
394 }
395
396 /// Returns the URL most recently passed to [`App::open_url`], or `None`.
397 ///
398 /// Nothing is launched. Only the last URL is kept, so open a second URL and the
399 /// first is no longer reported.
400 ///
401 /// # Example
402 ///
403 /// ```
404 /// # use gpui::TestApp;
405 /// let mut app = TestApp::new();
406 /// app.update(|cx| cx.open_url("https://example.com"));
407 /// assert_eq!(app.opened_url().as_deref(), Some("https://example.com"));
408 /// ```
409 pub fn opened_url(&self) -> Option<String> {
410 self.platform.opened_url.borrow().clone()
411 }
412
413 /// Returns whether a [`App::prompt_for_new_path`] request is waiting for an answer.
414 ///
415 /// Answer it with [`TestApp::simulate_new_path_selection`].
416 pub fn did_prompt_for_new_path(&self) -> bool {
417 self.platform.did_prompt_for_new_path()
418 }
419
420 /// Answers the oldest pending [`App::prompt_for_new_path`] request.
421 ///
422 /// `select` receives the directory the app asked about and returns the chosen
423 /// path, or `None` to cancel the request.
424 ///
425 /// # Panics
426 ///
427 /// Panics if no new-path request is pending.
428 pub fn simulate_new_path_selection(
429 &self,
430 select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
431 ) {
432 self.platform.simulate_new_path_selection(select);
433 }
434
435 /// Returns whether a prompt from [`Window::prompt`] is waiting for an answer.
436 pub fn has_pending_prompt(&self) -> bool {
437 self.platform.has_pending_prompt()
438 }
439
440 /// Answers the oldest pending prompt by pressing the button labelled `button`.
441 ///
442 /// The prompt is removed before the label is checked, so a wrong label still
443 /// consumes it.
444 ///
445 /// # Panics
446 ///
447 /// Panics if no prompt is pending, or if no button has the label `button`.
448 pub fn simulate_prompt_answer(&self, button: &str) {
449 self.platform.simulate_prompt_answer(button);
450 }
451
452 /// Returns the handles of all open windows.
453 pub fn windows(&self) -> Vec<AnyWindowHandle> {
454 self.read(|cx| cx.windows())
455 }
456}
457
458impl Default for TestApp {
459 fn default() -> Self {
460 Self::new()
461 }
462}
463
464/// A window opened by [`TestApp::open_window`].
465///
466/// Use it to read and update the root view and to send input to the window.
467/// Clones refer to the same window.
468///
469/// Each method that sends input or updates the view runs pending tasks before it
470/// returns, so the next line can assert on the result. The simulated input
471/// methods use the window's last painted frame to find elements, so call
472/// [`TestAppWindow::draw`] after building or changing the view.
473pub struct TestAppWindow<V> {
474 handle: WindowHandle<V>,
475 app: Rc<AppCell>,
476 platform: Rc<TestPlatform>,
477 background_executor: BackgroundExecutor,
478}
479
480impl<V: 'static + Render> TestAppWindow<V> {
481 /// Returns the window's handle, for APIs that take a [`WindowHandle`].
482 pub fn handle(&self) -> WindowHandle<V> {
483 self.handle
484 }
485
486 /// Returns the root view's entity.
487 ///
488 /// # Panics
489 ///
490 /// Panics if the window has been closed.
491 pub fn root(&self) -> Entity<V> {
492 let mut app = self.app.borrow_mut();
493 let any_handle: AnyWindowHandle = self.handle.into();
494 app.update_window(any_handle, |root_view, _, _| {
495 root_view.downcast::<V>().expect("root view type mismatch")
496 })
497 .expect("window not found")
498 }
499
500 /// Runs `f` with the root view, its [`Window`] and a [`Context`], then runs pending tasks until parked.
501 ///
502 /// # Panics
503 ///
504 /// Panics if the window has been closed.
505 pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
506 let result = {
507 let mut app = self.app.borrow_mut();
508 let any_handle: AnyWindowHandle = self.handle.into();
509 app.update_window(any_handle, |root_view, window, cx| {
510 let view = root_view.downcast::<V>().expect("root view type mismatch");
511 view.update(cx, |view, cx| f(view, window, cx))
512 })
513 .expect("window not found")
514 };
515 self.background_executor.run_until_parked();
516 result
517 }
518
519 /// Runs `f` with the root view and the [`App`], without running pending tasks.
520 ///
521 /// # Panics
522 ///
523 /// Panics if the window has been closed.
524 pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
525 let app = self.app.borrow();
526 let view = self
527 .app
528 .borrow()
529 .windows
530 .get(self.handle.window_id())
531 .and_then(|w| w.as_ref())
532 .and_then(|w| w.root.clone())
533 .and_then(|r| r.downcast::<V>().ok())
534 .expect("window or root view not found");
535 f(view.read(&app), &app)
536 }
537
538 /// Always returns `None`. The window title isn't available through this type yet.
539 ///
540 /// Don't use this to check a title. Tests that open windows with
541 /// [`TestAppContext::add_window_view`](crate::TestAppContext::add_window_view) can read the title with
542 /// [`VisualTestContext::window_title`](crate::VisualTestContext::window_title).
543 ///
544 /// # Panics
545 ///
546 /// Panics if the window has been closed.
547 ///
548 /// # Example
549 ///
550 /// ```
551 /// # use gpui::{Empty, TestApp};
552 /// let mut app = TestApp::new();
553 /// let window = app.open_window(|_, _| Empty);
554 /// assert_eq!(window.title(), None);
555 /// ```
556 pub fn title(&self) -> Option<String> {
557 let app = self.app.borrow();
558 app.read_window(&self.handle, |_, _cx| {
559 // TODO: expose title through Window API
560 None
561 })
562 .unwrap()
563 }
564
565 /// Sends one keystroke, such as `"ctrl-s"` or `"space"`, to the focused element, then runs pending tasks.
566 ///
567 /// Use the key name `"space"` for the space bar. A space character in the string
568 /// isn't a separator here; [`TestAppWindow::simulate_keystrokes`] splits on spaces.
569 ///
570 /// # Panics
571 ///
572 /// Panics if `keystroke` isn't a valid keystroke. See [keyboard input](crate::_documentation::guides::keyboard)
573 /// for the syntax.
574 ///
575 /// # Example
576 ///
577 /// ```
578 /// # use gpui::{Context, FocusHandle, KeyDownEvent, Render, TestApp, Window, div, prelude::*};
579 /// # struct Keys { focus: FocusHandle, last: Option<String> }
580 /// # impl Render for Keys {
581 /// # fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
582 /// # div().track_focus(&self.focus).on_key_down(cx.listener(|this, event: &KeyDownEvent, _, cx| {
583 /// # this.last = Some(event.keystroke.key.clone());
584 /// # cx.notify();
585 /// # }))
586 /// # }
587 /// # }
588 /// # let mut app = TestApp::new();
589 /// # let mut window = app.open_window(|window, cx| {
590 /// # let focus = cx.focus_handle();
591 /// # window.focus(&focus, cx);
592 /// # Keys { focus, last: None }
593 /// # });
594 /// window.simulate_keystroke("space");
595 /// window.read(|keys, _| assert_eq!(keys.last.as_deref(), Some("space")));
596 /// ```
597 pub fn simulate_keystroke(&mut self, keystroke: &str) {
598 let keystroke = Keystroke::parse(keystroke).unwrap();
599 {
600 let mut app = self.app.borrow_mut();
601 let any_handle: AnyWindowHandle = self.handle.into();
602 app.update_window(any_handle, |_, window, cx| {
603 window.dispatch_keystroke(keystroke, cx);
604 })
605 .unwrap();
606 }
607 self.background_executor.run_until_parked();
608 }
609
610 /// Sends each keystroke in a space-separated list, in order, then runs pending tasks after each one.
611 ///
612 /// Each part is parsed as in [`TestAppWindow::simulate_keystroke`]. A space
613 /// character in the list is a separator, so use `simulate_keystroke("space")` to
614 /// press the space bar. Two spaces in a row send an empty keystroke.
615 ///
616 /// # Panics
617 ///
618 /// Panics if any part isn't a valid keystroke.
619 ///
620 /// # Example
621 ///
622 /// ```
623 /// # use gpui::{Context, FocusHandle, KeyDownEvent, Render, TestApp, Window, div, prelude::*};
624 /// # struct Keys { focus: FocusHandle, last: Option<String> }
625 /// # impl Render for Keys {
626 /// # fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
627 /// # div().track_focus(&self.focus).on_key_down(cx.listener(|this, event: &KeyDownEvent, _, cx| {
628 /// # this.last = Some(event.keystroke.key.clone());
629 /// # cx.notify();
630 /// # }))
631 /// # }
632 /// # }
633 /// # let mut app = TestApp::new();
634 /// # let mut window = app.open_window(|window, cx| {
635 /// # let focus = cx.focus_handle();
636 /// # window.focus(&focus, cx);
637 /// # Keys { focus, last: None }
638 /// # });
639 /// window.simulate_keystrokes("a b");
640 /// window.read(|keys, _| assert_eq!(keys.last.as_deref(), Some("b")));
641 /// ```
642 pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
643 for keystroke in keystrokes.split(' ') {
644 self.simulate_keystroke(keystroke);
645 }
646 }
647
648 /// Types `input` by sending one keystroke per character, then runs pending tasks.
649 ///
650 /// Each character is sent as a keystroke whose key is that character, so text
651 /// inputs receive the text. Uppercase letters are sent with shift held. A space
652 /// is sent with the key `" "`, not `"space"`, so a binding for `space` doesn't
653 /// match it. Use [`TestAppWindow::simulate_keystroke`] for named keys.
654 ///
655 /// # Example
656 ///
657 /// ```
658 /// # use gpui::{Context, FocusHandle, KeyDownEvent, Render, TestApp, Window, div, prelude::*};
659 /// # struct Keys { focus: FocusHandle, last: Option<String> }
660 /// # impl Render for Keys {
661 /// # fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
662 /// # div().track_focus(&self.focus).on_key_down(cx.listener(|this, event: &KeyDownEvent, _, cx| {
663 /// # this.last = Some(event.keystroke.key.clone());
664 /// # cx.notify();
665 /// # }))
666 /// # }
667 /// # }
668 /// # let mut app = TestApp::new();
669 /// # let mut window = app.open_window(|window, cx| {
670 /// # let focus = cx.focus_handle();
671 /// # window.focus(&focus, cx);
672 /// # Keys { focus, last: None }
673 /// # });
674 /// window.simulate_input("xy");
675 /// window.read(|keys, _| assert_eq!(keys.last.as_deref(), Some("y")));
676 /// ```
677 pub fn simulate_input(&mut self, input: &str) {
678 for char in input.chars() {
679 self.simulate_keystroke(&char.to_string());
680 }
681 }
682
683 /// Moves the pointer to `position` with no button held, then runs pending tasks.
684 pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
685 self.simulate_event(MouseMoveEvent {
686 position,
687 modifiers: Default::default(),
688 pressed_button: None,
689 });
690 }
691
692 /// Presses `button` at `position`, then runs pending tasks.
693 ///
694 /// The click count is always `1`, so this never sends a double click.
695 pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
696 self.simulate_event(MouseDownEvent {
697 position,
698 button,
699 modifiers: Default::default(),
700 click_count: 1,
701 first_mouse: false,
702 });
703 }
704
705 /// Releases `button` at `position`, then runs pending tasks.
706 ///
707 /// The click count is always `1`.
708 pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
709 self.simulate_event(MouseUpEvent {
710 position,
711 button,
712 modifiers: Default::default(),
713 click_count: 1,
714 });
715 }
716
717 /// Presses and releases `button` at `position`, then runs pending tasks.
718 ///
719 /// Both events have a click count of `1`, so calling this twice in a row sends two
720 /// single clicks, never a double click. Call [`TestAppWindow::draw`] first, so
721 /// the click hits the element you expect. See the example on [`TestApp`].
722 pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
723 self.simulate_mouse_down(position, button);
724 self.simulate_mouse_up(position, button);
725 }
726
727 /// Scrolls by `delta`, in pixels, with the pointer at `position`, then runs pending tasks.
728 ///
729 /// No modifier keys are held.
730 pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
731 self.simulate_event(crate::ScrollWheelEvent {
732 position,
733 delta: crate::ScrollDelta::Pixels(delta),
734 modifiers: Default::default(),
735 touch_phase: crate::TouchPhase::Moved,
736 });
737 }
738
739 /// Dispatches `event` to the window as platform input, then runs pending tasks.
740 ///
741 /// Use this for input events that have no dedicated method on [`TestAppWindow`].
742 pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
743 let platform_input = event.to_platform_input();
744 {
745 let mut app = self.app.borrow_mut();
746 let any_handle: AnyWindowHandle = self.handle.into();
747 app.update_window(any_handle, |_, window, cx| {
748 window.dispatch_event(platform_input, cx);
749 })
750 .unwrap();
751 }
752 self.background_executor.run_until_parked();
753 }
754
755 /// Sets the window's size to `size`, then runs pending tasks.
756 ///
757 /// This runs the window's resize callback, if it has one, and updates the viewport that
758 /// `Window::viewport_size` reports. It doesn't paint the window: call
759 /// [`TestAppWindow::draw`] to paint at the new size.
760 pub fn simulate_resize(&mut self, size: Size<Pixels>) {
761 let window_id = self.handle.window_id();
762 // The window's resize callback updates the app, so it can't run while the app is borrowed.
763 let test_window = {
764 let mut app = self.app.borrow_mut();
765 match app.windows.get_mut(window_id) {
766 Some(Some(window)) => window.platform_window.as_test().cloned(),
767 _ => None,
768 }
769 };
770 if let Some(mut test_window) = test_window {
771 test_window.simulate_resize(size);
772 }
773 self.background_executor.run_until_parked();
774 }
775
776 /// Makes this the active window, as if the user had clicked it, then runs pending tasks.
777 ///
778 /// Windows that [`TestApp`] opens start inactive. Focus listeners such as
779 /// [`Context::on_focus`] only run in the active window, so activate it before testing them.
780 pub fn activate(&mut self) {
781 let window_id = self.handle.window_id();
782 let test_window = match self.app.borrow_mut().windows.get_mut(window_id) {
783 Some(Some(window)) => window.platform_window.as_test().cloned(),
784 _ => None,
785 };
786 self.platform.set_active_window(test_window);
787 self.background_executor.run_until_parked();
788 }
789
790 /// Simulate the window moving to a display with a different scale factor.
791 pub fn simulate_scale_factor_change(&mut self, scale_factor: f32) {
792 let window_id = self.handle.window_id();
793 let test_window = {
794 let mut app = self.app.borrow_mut();
795 app.windows
796 .get_mut(window_id)
797 .and_then(|window| window.as_mut())
798 .and_then(|window| window.platform_window.as_test())
799 .cloned()
800 };
801 // The resize callback needs to borrow the app again synchronously.
802 if let Some(mut test_window) = test_window {
803 test_window.simulate_scale_factor_change(scale_factor);
804 }
805 self.background_executor.run_until_parked();
806 }
807
808 /// Paints the window now, so that later input and hit tests use the current layout.
809 ///
810 /// Call this after building the view and after anything changes its layout, before
811 /// simulating a click at a position. It doesn't run pending tasks. See the example
812 /// on [`TestApp`].
813 pub fn draw(&mut self) {
814 let mut app = self.app.borrow_mut();
815 let any_handle: AnyWindowHandle = self.handle.into();
816 app.update_window(any_handle, |_, window, cx| {
817 window.draw(cx).clear(cx);
818 })
819 .unwrap();
820 }
821}
822
823impl<V> Clone for TestAppWindow<V> {
824 fn clone(&self) -> Self {
825 Self {
826 handle: self.handle,
827 app: self.app.clone(),
828 platform: self.platform.clone(),
829 background_executor: self.background_executor.clone(),
830 }
831 }
832}
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837 use crate::{FocusHandle, Focusable, div, prelude::*, px, size};
838
839 struct Counter {
840 count: usize,
841 focus_handle: FocusHandle,
842 }
843
844 impl Counter {
845 fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
846 let focus_handle = cx.focus_handle();
847 Self {
848 count: 0,
849 focus_handle,
850 }
851 }
852
853 fn increment(&mut self, _cx: &mut Context<Self>) {
854 self.count += 1;
855 }
856 }
857
858 impl Focusable for Counter {
859 fn focus_handle(&self, _cx: &App) -> FocusHandle {
860 self.focus_handle.clone()
861 }
862 }
863
864 impl Render for Counter {
865 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
866 div().child(format!("Count: {}", self.count))
867 }
868 }
869
870 #[test]
871 fn test_basic_usage() {
872 let mut app = TestApp::new();
873
874 let mut window = app.open_window(Counter::new);
875
876 window.update(|counter, _window, cx| {
877 counter.increment(cx);
878 });
879
880 window.read(|counter, _| {
881 assert_eq!(counter.count, 1);
882 });
883
884 drop(window);
885 app.update(|cx| cx.shutdown());
886 }
887
888 #[test]
889 fn test_resize_updates_gpui_viewport() {
890 let mut app = TestApp::new();
891 let mut window = app.open_window(Counter::new);
892
893 window.simulate_resize(size(px(321.), px(123.)));
894
895 window.update(|_, window, _| {
896 assert_eq!(window.viewport_size(), size(px(321.), px(123.)));
897 });
898 }
899
900 #[test]
901 fn test_simulate_scale_factor_change() {
902 let mut app = TestApp::new();
903 let mut window = app.open_window(|_, _| crate::EmptyView);
904 let viewport_size = window.update(|_, window, _| {
905 assert_eq!(window.scale_factor(), 2.0);
906 window.viewport_size()
907 });
908
909 window.simulate_scale_factor_change(1.0);
910
911 window.update(|_, window, _| {
912 assert_eq!(window.scale_factor(), 1.0);
913 assert_eq!(window.viewport_size(), viewport_size);
914 });
915
916 drop(window);
917 app.update(|cx| cx.shutdown());
918 }
919
920 #[test]
921 fn test_entity_creation() {
922 let mut app = TestApp::new();
923
924 let entity = app.new_entity(|cx| Counter {
925 count: 42,
926 focus_handle: cx.focus_handle(),
927 });
928
929 app.read_entity(&entity, |counter, _| {
930 assert_eq!(counter.count, 42);
931 });
932
933 app.update_entity(&entity, |counter, _cx| {
934 counter.count += 1;
935 });
936
937 app.read_entity(&entity, |counter, _| {
938 assert_eq!(counter.count, 43);
939 });
940 }
941
942 #[test]
943 fn test_globals() {
944 let mut app = TestApp::new();
945
946 struct MyGlobal(String);
947 impl Global for MyGlobal {}
948
949 assert!(!app.has_global::<MyGlobal>());
950
951 app.set_global(MyGlobal("hello".into()));
952
953 assert!(app.has_global::<MyGlobal>());
954
955 app.read_global::<MyGlobal, _>(|global, _| {
956 assert_eq!(global.0, "hello");
957 });
958
959 app.update_global::<MyGlobal, _>(|global, _| {
960 global.0 = "world".into();
961 });
962
963 app.read_global::<MyGlobal, _>(|global, _| {
964 assert_eq!(global.0, "world");
965 });
966 }
967}