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