1use crate::{
2 Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor,
3 Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
4 Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point,
5 Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, Window,
6 WindowBounds, WindowHandle, WindowOptions, app::GpuiMode,
7};
8use anyhow::anyhow;
9use image::RgbaImage;
10use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
11
12/// A test context that uses real macOS rendering instead of mocked rendering.
13/// This is used for visual tests that need to capture actual screenshots.
14///
15/// Unlike `TestAppContext` which uses `TestPlatform` with mocked rendering,
16/// `VisualTestAppContext` uses the real `MacPlatform` to produce actual rendered output.
17///
18/// Windows created through this context are positioned off-screen (at coordinates like -10000, -10000)
19/// so they are invisible to the user but still fully rendered by the compositor.
20#[derive(Clone)]
21pub struct VisualTestAppContext {
22 /// The underlying app cell
23 pub app: Rc<AppCell>,
24 /// The background executor for running async tasks
25 pub background_executor: BackgroundExecutor,
26 /// The foreground executor for running tasks on the main thread
27 pub foreground_executor: ForegroundExecutor,
28 /// The test dispatcher for deterministic task scheduling
29 dispatcher: TestDispatcher,
30 platform: Rc<dyn Platform>,
31 text_system: Arc<TextSystem>,
32}
33
34impl VisualTestAppContext {
35 /// Creates a new `VisualTestAppContext` with real macOS platform rendering
36 /// but deterministic task scheduling via TestDispatcher.
37 ///
38 /// This provides:
39 /// - Real Metal/compositor rendering for accurate screenshots
40 /// - Deterministic task scheduling via TestDispatcher
41 /// - Controllable time via `advance_clock`
42 ///
43 /// Note: This uses a no-op asset source, so SVG icons won't render.
44 /// Use `with_asset_source` to provide real assets for icon rendering.
45 pub fn new() -> Self {
46 Self::with_asset_source(Arc::new(()))
47 }
48
49 /// Creates a new `VisualTestAppContext` with a custom asset source.
50 ///
51 /// Use this when you need SVG icons to render properly in visual tests.
52 /// Pass the real `Assets` struct to enable icon rendering.
53 pub fn with_asset_source(asset_source: Arc<dyn AssetSource>) -> Self {
54 // Use a seeded RNG for deterministic behavior
55 let seed = std::env::var("SEED")
56 .ok()
57 .and_then(|s| s.parse().ok())
58 .unwrap_or(0);
59
60 // Create a visual test platform that combines real Mac rendering
61 // with controllable TestDispatcher for deterministic task scheduling
62 let platform = Rc::new(VisualTestPlatform::new(seed));
63
64 // Get the dispatcher and executors from the platform
65 let dispatcher = platform.dispatcher().clone();
66 let background_executor = platform.background_executor();
67 let foreground_executor = platform.foreground_executor();
68
69 let text_system = Arc::new(TextSystem::new(platform.text_system()));
70
71 let http_client = http_client::FakeHttpClient::with_404_response();
72
73 let mut app = App::new_app(platform.clone(), asset_source, http_client);
74 app.borrow_mut().mode = GpuiMode::test();
75
76 Self {
77 app,
78 background_executor,
79 foreground_executor,
80 dispatcher,
81 platform,
82 text_system,
83 }
84 }
85
86 /// Opens a window positioned off-screen for invisible rendering.
87 ///
88 /// The window is positioned at (-10000, -10000) so it's not visible on any display,
89 /// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit.
90 ///
91 /// # Arguments
92 /// * `size` - The size of the window to create
93 /// * `build_root` - A closure that builds the root view for the window
94 pub fn open_offscreen_window<V: Render + 'static>(
95 &mut self,
96 size: Size<Pixels>,
97 build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
98 ) -> Result<WindowHandle<V>> {
99 use crate::{point, px};
100
101 let bounds = Bounds {
102 origin: point(px(-10000.0), px(-10000.0)),
103 size,
104 };
105
106 let mut cx = self.app.borrow_mut();
107 cx.open_window(
108 WindowOptions {
109 window_bounds: Some(WindowBounds::Windowed(bounds)),
110 focus: false,
111 show: true,
112 ..Default::default()
113 },
114 build_root,
115 )
116 }
117
118 /// Opens an off-screen window with default size (1280x800).
119 pub fn open_offscreen_window_default<V: Render + 'static>(
120 &mut self,
121 build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
122 ) -> Result<WindowHandle<V>> {
123 use crate::{px, size};
124 self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root)
125 }
126
127 /// Returns whether screen capture is supported on this platform.
128 pub fn is_screen_capture_supported(&self) -> bool {
129 self.platform.is_screen_capture_supported()
130 }
131
132 /// Returns the text system used by this context.
133 pub fn text_system(&self) -> &Arc<TextSystem> {
134 &self.text_system
135 }
136
137 /// Returns the background executor.
138 pub fn executor(&self) -> BackgroundExecutor {
139 self.background_executor.clone()
140 }
141
142 /// Returns the foreground executor.
143 pub fn foreground_executor(&self) -> ForegroundExecutor {
144 self.foreground_executor.clone()
145 }
146
147 /// Runs all pending foreground and background tasks until there's nothing left to do.
148 /// This is essential for processing async operations like tooltip timers.
149 pub fn run_until_parked(&self) {
150 self.dispatcher.run_until_parked();
151 }
152
153 /// Advances the simulated clock by the given duration and processes any tasks
154 /// that become ready. This is essential for testing time-based behaviors like
155 /// tooltip delays.
156 pub fn advance_clock(&self, duration: Duration) {
157 self.dispatcher.advance_clock(duration);
158 }
159
160 /// Updates the app state.
161 pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
162 let mut app = self.app.borrow_mut();
163 f(&mut app)
164 }
165
166 /// Reads from the app state.
167 pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
168 let app = self.app.borrow();
169 f(&app)
170 }
171
172 /// Updates a window.
173 pub fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
174 where
175 F: FnOnce(AnyView, &mut Window, &mut App) -> T,
176 {
177 let mut lock = self.app.borrow_mut();
178 lock.update_window(window, f)
179 }
180
181 /// Spawns a task on the foreground executor.
182 pub fn spawn<F, R>(&self, f: F) -> Task<R>
183 where
184 F: Future<Output = R> + 'static,
185 R: 'static,
186 {
187 self.foreground_executor.spawn(f)
188 }
189
190 /// Checks if a global of type G exists.
191 pub fn has_global<G: Global>(&self) -> bool {
192 let app = self.app.borrow();
193 app.has_global::<G>()
194 }
195
196 /// Reads a global value.
197 pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
198 let app = self.app.borrow();
199 f(app.global::<G>(), &app)
200 }
201
202 /// Sets a global value.
203 pub fn set_global<G: Global>(&mut self, global: G) {
204 let mut app = self.app.borrow_mut();
205 app.set_global(global);
206 }
207
208 /// Updates a global value.
209 pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
210 let mut lock = self.app.borrow_mut();
211 lock.update(|cx| {
212 let mut global = cx.lease_global::<G>();
213 let result = f(&mut global, cx);
214 cx.end_global_lease(global);
215 result
216 })
217 }
218
219 /// Simulates a sequence of keystrokes on the given window.
220 ///
221 /// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape".
222 pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
223 for keystroke_text in keystrokes.split_whitespace() {
224 let keystroke = Keystroke::parse(keystroke_text)
225 .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text));
226 self.dispatch_keystroke(window, keystroke);
227 }
228 self.run_until_parked();
229 }
230
231 /// Dispatches a single keystroke to a window.
232 pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
233 self.update_window(window, |_, window, cx| {
234 window.dispatch_keystroke(keystroke, cx);
235 })
236 .ok();
237 }
238
239 /// Simulates typing text input on the given window.
240 pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
241 for char in input.chars() {
242 let key = char.to_string();
243 let keystroke = Keystroke {
244 modifiers: Modifiers::default(),
245 key: key.clone(),
246 key_char: Some(key),
247 };
248 self.dispatch_keystroke(window, keystroke);
249 }
250 self.run_until_parked();
251 }
252
253 /// Simulates a mouse move event.
254 pub fn simulate_mouse_move(
255 &mut self,
256 window: AnyWindowHandle,
257 position: Point<Pixels>,
258 button: impl Into<Option<MouseButton>>,
259 modifiers: Modifiers,
260 ) {
261 self.simulate_event(
262 window,
263 MouseMoveEvent {
264 position,
265 modifiers,
266 pressed_button: button.into(),
267 },
268 );
269 }
270
271 /// Simulates a mouse down event.
272 pub fn simulate_mouse_down(
273 &mut self,
274 window: AnyWindowHandle,
275 position: Point<Pixels>,
276 button: MouseButton,
277 modifiers: Modifiers,
278 ) {
279 self.simulate_event(
280 window,
281 MouseDownEvent {
282 position,
283 modifiers,
284 button,
285 click_count: 1,
286 first_mouse: false,
287 },
288 );
289 }
290
291 /// Simulates a mouse up event.
292 pub fn simulate_mouse_up(
293 &mut self,
294 window: AnyWindowHandle,
295 position: Point<Pixels>,
296 button: MouseButton,
297 modifiers: Modifiers,
298 ) {
299 self.simulate_event(
300 window,
301 MouseUpEvent {
302 position,
303 modifiers,
304 button,
305 click_count: 1,
306 },
307 );
308 }
309
310 /// Simulates a click (mouse down followed by mouse up).
311 pub fn simulate_click(
312 &mut self,
313 window: AnyWindowHandle,
314 position: Point<Pixels>,
315 modifiers: Modifiers,
316 ) {
317 self.simulate_mouse_down(window, position, MouseButton::Left, modifiers);
318 self.simulate_mouse_up(window, position, MouseButton::Left, modifiers);
319 }
320
321 /// Simulates an input event on the given window.
322 pub fn simulate_event<E: InputEvent>(&mut self, window: AnyWindowHandle, event: E) {
323 self.update_window(window, |_, window, cx| {
324 window.dispatch_event(event.to_platform_input(), cx);
325 })
326 .ok();
327 self.run_until_parked();
328 }
329
330 /// Dispatches an action to the given window.
331 pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) {
332 self.update_window(window, |_, window, cx| {
333 window.dispatch_action(action.boxed_clone(), cx);
334 })
335 .ok();
336 self.run_until_parked();
337 }
338
339 /// Writes to the clipboard.
340 pub fn write_to_clipboard(&self, item: ClipboardItem) {
341 self.platform.write_to_clipboard(item);
342 }
343
344 /// Reads from the clipboard.
345 pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
346 self.platform.read_from_clipboard()
347 }
348
349 /// Waits for a condition to become true, with a timeout.
350 pub async fn wait_for<T: 'static>(
351 &mut self,
352 entity: &Entity<T>,
353 predicate: impl Fn(&T) -> bool,
354 timeout: Duration,
355 ) -> Result<()> {
356 let start = std::time::Instant::now();
357 loop {
358 {
359 let app = self.app.borrow();
360 if predicate(entity.read(&app)) {
361 return Ok(());
362 }
363 }
364
365 if start.elapsed() > timeout {
366 return Err(anyhow!("Timed out waiting for condition"));
367 }
368
369 self.run_until_parked();
370 self.background_executor
371 .timer(Duration::from_millis(10))
372 .await;
373 }
374 }
375
376 /// Captures a screenshot of the specified window using direct texture capture.
377 ///
378 /// This renders the scene to a Metal texture and reads the pixels directly,
379 /// which does not require the window to be visible on screen.
380 #[cfg(any(test, feature = "test-support"))]
381 pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
382 self.update_window(window, |_, window, _cx| window.render_to_image())?
383 }
384
385 /// Waits for animations to complete by waiting a couple of frames.
386 pub async fn wait_for_animations(&self) {
387 self.background_executor
388 .timer(Duration::from_millis(32))
389 .await;
390 self.run_until_parked();
391 }
392}
393
394impl Default for VisualTestAppContext {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400impl AppContext for VisualTestAppContext {
401 fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
402 let mut app = self.app.borrow_mut();
403 app.new(build_entity)
404 }
405
406 fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
407 let mut app = self.app.borrow_mut();
408 app.reserve_entity()
409 }
410
411 fn insert_entity<T: 'static>(
412 &mut self,
413 reservation: crate::Reservation<T>,
414 build_entity: impl FnOnce(&mut Context<T>) -> T,
415 ) -> Entity<T> {
416 let mut app = self.app.borrow_mut();
417 app.insert_entity(reservation, build_entity)
418 }
419
420 fn update_entity<T: 'static, R>(
421 &mut self,
422 handle: &Entity<T>,
423 update: impl FnOnce(&mut T, &mut Context<T>) -> R,
424 ) -> R {
425 let mut app = self.app.borrow_mut();
426 app.update_entity(handle, update)
427 }
428
429 fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> crate::GpuiBorrow<'a, T>
430 where
431 T: 'static,
432 {
433 panic!("Cannot use as_mut with a visual test app context. Try calling update() first")
434 }
435
436 fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
437 where
438 T: 'static,
439 {
440 let app = self.app.borrow();
441 app.read_entity(handle, read)
442 }
443
444 fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
445 where
446 F: FnOnce(AnyView, &mut Window, &mut App) -> T,
447 {
448 let mut lock = self.app.borrow_mut();
449 lock.update_window(window, f)
450 }
451
452 fn read_window<T, R>(
453 &self,
454 window: &WindowHandle<T>,
455 read: impl FnOnce(Entity<T>, &App) -> R,
456 ) -> Result<R>
457 where
458 T: 'static,
459 {
460 let app = self.app.borrow();
461 app.read_window(window, read)
462 }
463
464 fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
465 where
466 R: Send + 'static,
467 {
468 self.background_executor.spawn(future)
469 }
470
471 fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
472 where
473 G: Global,
474 {
475 let app = self.app.borrow();
476 callback(app.global::<G>(), &app)
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use crate::Empty;
484 use std::cell::RefCell;
485
486 // Note: All VisualTestAppContext tests are ignored by default because they require
487 // the macOS main thread. Standard Rust tests run on worker threads, which causes
488 // SIGABRT when interacting with macOS AppKit/Cocoa APIs.
489 //
490 // To run these tests, use:
491 // cargo test -p gpui visual_test_context -- --ignored --test-threads=1
492
493 #[test]
494 #[ignore] // Requires macOS main thread
495 fn test_foreground_tasks_run_with_run_until_parked() {
496 let mut cx = VisualTestAppContext::new();
497
498 let task_ran = Rc::new(RefCell::new(false));
499
500 // Spawn a foreground task via the App's spawn method
501 // This should use our TestDispatcher, not the MacDispatcher
502 {
503 let task_ran = task_ran.clone();
504 cx.update(|cx| {
505 cx.spawn(async move |_| {
506 *task_ran.borrow_mut() = true;
507 })
508 .detach();
509 });
510 }
511
512 // The task should not have run yet
513 assert!(!*task_ran.borrow());
514
515 // Run until parked should execute the foreground task
516 cx.run_until_parked();
517
518 // Now the task should have run
519 assert!(*task_ran.borrow());
520 }
521
522 #[test]
523 #[ignore] // Requires macOS main thread
524 fn test_advance_clock_triggers_delayed_tasks() {
525 let mut cx = VisualTestAppContext::new();
526
527 let task_ran = Rc::new(RefCell::new(false));
528
529 // Spawn a task that waits for a timer
530 {
531 let task_ran = task_ran.clone();
532 let executor = cx.background_executor.clone();
533 cx.update(|cx| {
534 cx.spawn(async move |_| {
535 executor.timer(Duration::from_millis(500)).await;
536 *task_ran.borrow_mut() = true;
537 })
538 .detach();
539 });
540 }
541
542 // Run until parked - the task should be waiting on the timer
543 cx.run_until_parked();
544 assert!(!*task_ran.borrow());
545
546 // Advance clock past the timer duration
547 cx.advance_clock(Duration::from_millis(600));
548
549 // Now the task should have completed
550 assert!(*task_ran.borrow());
551 }
552
553 #[test]
554 #[ignore] // Requires macOS main thread - window creation fails on test threads
555 fn test_window_spawn_uses_test_dispatcher() {
556 let mut cx = VisualTestAppContext::new();
557
558 let task_ran = Rc::new(RefCell::new(false));
559
560 let window = cx
561 .open_offscreen_window_default(|_, cx| cx.new(|_| Empty))
562 .expect("Failed to open window");
563
564 // Spawn a task via window.spawn - this is the critical test case
565 // for tooltip behavior, as tooltips use window.spawn for delayed show
566 {
567 let task_ran = task_ran.clone();
568 cx.update_window(window.into(), |_, window, cx| {
569 window
570 .spawn(cx, async move |_| {
571 *task_ran.borrow_mut() = true;
572 })
573 .detach();
574 })
575 .ok();
576 }
577
578 // The task should not have run yet
579 assert!(!*task_ran.borrow());
580
581 // Run until parked should execute the foreground task spawned via window
582 cx.run_until_parked();
583
584 // Now the task should have run
585 assert!(*task_ran.borrow());
586 }
587}