1use crate::{
2 Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, Bounds,
3 ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
4 MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
5 Result, Size, Task, TextSystem, Window, WindowBounds, WindowHandle, WindowOptions,
6 app::GpuiMode, current_platform,
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 platform: Rc<dyn Platform>,
29 text_system: Arc<TextSystem>,
30}
31
32impl VisualTestAppContext {
33 /// Creates a new `VisualTestAppContext` with real macOS platform rendering.
34 ///
35 /// This initializes the real macOS platform (not the test platform), which means:
36 /// - Windows are actually rendered by Metal/the compositor
37 /// - Screenshots can be captured via ScreenCaptureKit
38 /// - All platform APIs work as they do in production
39 pub fn new() -> Self {
40 let liveness = Arc::new(());
41 let liveness_weak = Arc::downgrade(&liveness);
42 let platform = current_platform(false, liveness_weak);
43 let background_executor = platform.background_executor();
44 let foreground_executor = platform.foreground_executor();
45 let text_system = Arc::new(TextSystem::new(platform.text_system()));
46
47 let asset_source = Arc::new(());
48 let http_client = http_client::FakeHttpClient::with_404_response();
49
50 let mut app = App::new_app(platform.clone(), liveness, asset_source, http_client);
51 app.borrow_mut().mode = GpuiMode::test();
52
53 Self {
54 app,
55 background_executor,
56 foreground_executor,
57 platform,
58 text_system,
59 }
60 }
61
62 /// Opens a window positioned off-screen for invisible rendering.
63 ///
64 /// The window is positioned at (-10000, -10000) so it's not visible on any display,
65 /// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit.
66 ///
67 /// # Arguments
68 /// * `size` - The size of the window to create
69 /// * `build_root` - A closure that builds the root view for the window
70 pub fn open_offscreen_window<V: Render + 'static>(
71 &mut self,
72 size: Size<Pixels>,
73 build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
74 ) -> Result<WindowHandle<V>> {
75 use crate::{point, px};
76
77 let bounds = Bounds {
78 origin: point(px(-10000.0), px(-10000.0)),
79 size,
80 };
81
82 let mut cx = self.app.borrow_mut();
83 cx.open_window(
84 WindowOptions {
85 window_bounds: Some(WindowBounds::Windowed(bounds)),
86 focus: false,
87 show: true,
88 ..Default::default()
89 },
90 build_root,
91 )
92 }
93
94 /// Opens an off-screen window with default size (1280x800).
95 pub fn open_offscreen_window_default<V: Render + 'static>(
96 &mut self,
97 build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
98 ) -> Result<WindowHandle<V>> {
99 use crate::{px, size};
100 self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root)
101 }
102
103 /// Returns whether screen capture is supported on this platform.
104 pub fn is_screen_capture_supported(&self) -> bool {
105 self.platform.is_screen_capture_supported()
106 }
107
108 /// Returns the text system used by this context.
109 pub fn text_system(&self) -> &Arc<TextSystem> {
110 &self.text_system
111 }
112
113 /// Returns the background executor.
114 pub fn executor(&self) -> BackgroundExecutor {
115 self.background_executor.clone()
116 }
117
118 /// Returns the foreground executor.
119 pub fn foreground_executor(&self) -> ForegroundExecutor {
120 self.foreground_executor.clone()
121 }
122
123 /// Runs pending background tasks until there's nothing left to do.
124 pub fn run_until_parked(&self) {
125 self.background_executor.run_until_parked();
126 }
127
128 /// Updates the app state.
129 pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
130 let mut app = self.app.borrow_mut();
131 f(&mut app)
132 }
133
134 /// Reads from the app state.
135 pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
136 let app = self.app.borrow();
137 f(&app)
138 }
139
140 /// Updates a window.
141 pub fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
142 where
143 F: FnOnce(AnyView, &mut Window, &mut App) -> T,
144 {
145 let mut lock = self.app.borrow_mut();
146 lock.update_window(window, f)
147 }
148
149 /// Spawns a task on the foreground executor.
150 pub fn spawn<F, R>(&self, f: F) -> Task<R>
151 where
152 F: Future<Output = R> + 'static,
153 R: 'static,
154 {
155 self.foreground_executor.spawn(f)
156 }
157
158 /// Checks if a global of type G exists.
159 pub fn has_global<G: Global>(&self) -> bool {
160 let app = self.app.borrow();
161 app.has_global::<G>()
162 }
163
164 /// Reads a global value.
165 pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
166 let app = self.app.borrow();
167 f(app.global::<G>(), &app)
168 }
169
170 /// Sets a global value.
171 pub fn set_global<G: Global>(&mut self, global: G) {
172 let mut app = self.app.borrow_mut();
173 app.set_global(global);
174 }
175
176 /// Updates a global value.
177 pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
178 let mut lock = self.app.borrow_mut();
179 lock.update(|cx| {
180 let mut global = cx.lease_global::<G>();
181 let result = f(&mut global, cx);
182 cx.end_global_lease(global);
183 result
184 })
185 }
186
187 /// Simulates a sequence of keystrokes on the given window.
188 ///
189 /// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape".
190 pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
191 for keystroke_text in keystrokes.split_whitespace() {
192 let keystroke = Keystroke::parse(keystroke_text)
193 .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text));
194 self.dispatch_keystroke(window, keystroke);
195 }
196 self.run_until_parked();
197 }
198
199 /// Dispatches a single keystroke to a window.
200 pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
201 self.update_window(window, |_, window, cx| {
202 window.dispatch_keystroke(keystroke, cx);
203 })
204 .ok();
205 }
206
207 /// Simulates typing text input on the given window.
208 pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
209 for char in input.chars() {
210 let key = char.to_string();
211 let keystroke = Keystroke {
212 modifiers: Modifiers::default(),
213 key: key.clone(),
214 key_char: Some(key),
215 };
216 self.dispatch_keystroke(window, keystroke);
217 }
218 self.run_until_parked();
219 }
220
221 /// Simulates a mouse move event.
222 pub fn simulate_mouse_move(
223 &mut self,
224 window: AnyWindowHandle,
225 position: Point<Pixels>,
226 button: impl Into<Option<MouseButton>>,
227 modifiers: Modifiers,
228 ) {
229 self.simulate_event(
230 window,
231 MouseMoveEvent {
232 position,
233 modifiers,
234 pressed_button: button.into(),
235 },
236 );
237 }
238
239 /// Simulates a mouse down event.
240 pub fn simulate_mouse_down(
241 &mut self,
242 window: AnyWindowHandle,
243 position: Point<Pixels>,
244 button: MouseButton,
245 modifiers: Modifiers,
246 ) {
247 self.simulate_event(
248 window,
249 MouseDownEvent {
250 position,
251 modifiers,
252 button,
253 click_count: 1,
254 first_mouse: false,
255 },
256 );
257 }
258
259 /// Simulates a mouse up event.
260 pub fn simulate_mouse_up(
261 &mut self,
262 window: AnyWindowHandle,
263 position: Point<Pixels>,
264 button: MouseButton,
265 modifiers: Modifiers,
266 ) {
267 self.simulate_event(
268 window,
269 MouseUpEvent {
270 position,
271 modifiers,
272 button,
273 click_count: 1,
274 },
275 );
276 }
277
278 /// Simulates a click (mouse down followed by mouse up).
279 pub fn simulate_click(
280 &mut self,
281 window: AnyWindowHandle,
282 position: Point<Pixels>,
283 modifiers: Modifiers,
284 ) {
285 self.simulate_mouse_down(window, position, MouseButton::Left, modifiers);
286 self.simulate_mouse_up(window, position, MouseButton::Left, modifiers);
287 }
288
289 /// Simulates an input event on the given window.
290 pub fn simulate_event<E: InputEvent>(&mut self, window: AnyWindowHandle, event: E) {
291 self.update_window(window, |_, window, cx| {
292 window.dispatch_event(event.to_platform_input(), cx);
293 })
294 .ok();
295 self.run_until_parked();
296 }
297
298 /// Dispatches an action to the given window.
299 pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) {
300 self.update_window(window, |_, window, cx| {
301 window.dispatch_action(action.boxed_clone(), cx);
302 })
303 .ok();
304 self.run_until_parked();
305 }
306
307 /// Writes to the clipboard.
308 pub fn write_to_clipboard(&self, item: ClipboardItem) {
309 self.platform.write_to_clipboard(item);
310 }
311
312 /// Reads from the clipboard.
313 pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
314 self.platform.read_from_clipboard()
315 }
316
317 /// Waits for a condition to become true, with a timeout.
318 pub async fn wait_for<T: 'static>(
319 &mut self,
320 entity: &Entity<T>,
321 predicate: impl Fn(&T) -> bool,
322 timeout: Duration,
323 ) -> Result<()> {
324 let start = std::time::Instant::now();
325 loop {
326 {
327 let app = self.app.borrow();
328 if predicate(entity.read(&app)) {
329 return Ok(());
330 }
331 }
332
333 if start.elapsed() > timeout {
334 return Err(anyhow!("Timed out waiting for condition"));
335 }
336
337 self.run_until_parked();
338 self.background_executor
339 .timer(Duration::from_millis(10))
340 .await;
341 }
342 }
343
344 /// Captures a screenshot of the specified window using direct texture capture.
345 ///
346 /// This renders the scene to a Metal texture and reads the pixels directly,
347 /// which does not require the window to be visible on screen.
348 #[cfg(any(test, feature = "test-support"))]
349 pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
350 self.update_window(window, |_, window, _cx| window.render_to_image())?
351 }
352
353 /// Waits for animations to complete by waiting a couple of frames.
354 pub async fn wait_for_animations(&self) {
355 self.background_executor
356 .timer(Duration::from_millis(32))
357 .await;
358 self.run_until_parked();
359 }
360}
361
362impl Default for VisualTestAppContext {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368impl AppContext for VisualTestAppContext {
369 type Result<T> = T;
370
371 fn new<T: 'static>(
372 &mut self,
373 build_entity: impl FnOnce(&mut Context<T>) -> T,
374 ) -> Self::Result<Entity<T>> {
375 let mut app = self.app.borrow_mut();
376 app.new(build_entity)
377 }
378
379 fn reserve_entity<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
380 let mut app = self.app.borrow_mut();
381 app.reserve_entity()
382 }
383
384 fn insert_entity<T: 'static>(
385 &mut self,
386 reservation: crate::Reservation<T>,
387 build_entity: impl FnOnce(&mut Context<T>) -> T,
388 ) -> Self::Result<Entity<T>> {
389 let mut app = self.app.borrow_mut();
390 app.insert_entity(reservation, build_entity)
391 }
392
393 fn update_entity<T: 'static, R>(
394 &mut self,
395 handle: &Entity<T>,
396 update: impl FnOnce(&mut T, &mut Context<T>) -> R,
397 ) -> Self::Result<R> {
398 let mut app = self.app.borrow_mut();
399 app.update_entity(handle, update)
400 }
401
402 fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<crate::GpuiBorrow<'a, T>>
403 where
404 T: 'static,
405 {
406 panic!("Cannot use as_mut with a visual test app context. Try calling update() first")
407 }
408
409 fn read_entity<T, R>(
410 &self,
411 handle: &Entity<T>,
412 read: impl FnOnce(&T, &App) -> R,
413 ) -> Self::Result<R>
414 where
415 T: 'static,
416 {
417 let app = self.app.borrow();
418 app.read_entity(handle, read)
419 }
420
421 fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
422 where
423 F: FnOnce(AnyView, &mut Window, &mut App) -> T,
424 {
425 let mut lock = self.app.borrow_mut();
426 lock.update_window(window, f)
427 }
428
429 fn read_window<T, R>(
430 &self,
431 window: &WindowHandle<T>,
432 read: impl FnOnce(Entity<T>, &App) -> R,
433 ) -> Result<R>
434 where
435 T: 'static,
436 {
437 let app = self.app.borrow();
438 app.read_window(window, read)
439 }
440
441 fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
442 where
443 R: Send + 'static,
444 {
445 self.background_executor.spawn(future)
446 }
447
448 fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
449 where
450 G: Global,
451 {
452 let app = self.app.borrow();
453 callback(app.global::<G>(), &app)
454 }
455}