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