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