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