Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:49:13.558Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

test_context.rs

1338 lines · 44.5 KB · rust
1use crate::{
2    Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace,
3    BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
4    Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke,
5    Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
6    Pixels, Platform, Point, Render, Result, SharedString, Size, SystemNotification,
7    SystemNotificationResponse, Task, TestDispatcher, TestPlatform, TestScreenCaptureSource,
8    TestWindow, TextSystem, VisualContext, Window, WindowBounds, WindowHandle, WindowOptions,
9    app::GpuiMode, window::ElementArenaScope,
10};
11use anyhow::{anyhow, bail};
12use futures::{Stream, StreamExt, channel::oneshot};
13
14use std::{
15    cell::RefCell, future::Future, ops::Deref, path::PathBuf, rc::Rc, sync::Arc, time::Duration,
16};
17
18/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
19/// an implementation of `Context` with additional methods that are useful in tests.
20#[derive(Clone)]
21pub struct TestAppContext {
22    #[doc(hidden)]
23    pub background_executor: BackgroundExecutor,
24    #[doc(hidden)]
25    pub foreground_executor: ForegroundExecutor,
26    #[doc(hidden)]
27    pub dispatcher: TestDispatcher,
28    test_platform: Rc<TestPlatform>,
29    text_system: Arc<TextSystem>,
30    fn_name: Option<&'static str>,
31    on_quit: Rc<RefCell<Vec<Box<dyn FnOnce() + 'static>>>>,
32    #[doc(hidden)]
33    pub app: Rc<AppCell>,
34}
35
36impl AppContext for TestAppContext {
37    fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
38        let mut app = self.app.borrow_mut();
39        app.new(build_entity)
40    }
41
42    fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
43        let mut app = self.app.borrow_mut();
44        app.reserve_entity()
45    }
46
47    fn insert_entity<T: 'static>(
48        &mut self,
49        reservation: crate::Reservation<T>,
50        build_entity: impl FnOnce(&mut Context<T>) -> T,
51    ) -> Entity<T> {
52        let mut app = self.app.borrow_mut();
53        app.insert_entity(reservation, build_entity)
54    }
55
56    fn update_entity<T: 'static, R>(
57        &mut self,
58        handle: &Entity<T>,
59        update: impl FnOnce(&mut T, &mut Context<T>) -> R,
60    ) -> R {
61        let mut app = self.app.borrow_mut();
62        app.update_entity(handle, update)
63    }
64
65    fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> super::GpuiBorrow<'a, T>
66    where
67        T: 'static,
68    {
69        panic!("Cannot use as_mut with a test app context. Try calling update() first")
70    }
71
72    fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
73    where
74        T: 'static,
75    {
76        let app = self.app.borrow();
77        app.read_entity(handle, read)
78    }
79
80    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
81    where
82        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
83    {
84        let mut lock = self.app.borrow_mut();
85        lock.update_window(window, f)
86    }
87
88    fn with_window<R>(
89        &mut self,
90        entity_id: EntityId,
91        f: impl FnOnce(&mut Window, &mut App) -> R,
92    ) -> Option<R> {
93        let mut lock = self.app.borrow_mut();
94        lock.with_window(entity_id, f)
95    }
96
97    fn read_window<T, R>(
98        &self,
99        window: &WindowHandle<T>,
100        read: impl FnOnce(Entity<T>, &App) -> R,
101    ) -> Result<R>
102    where
103        T: 'static,
104    {
105        let app = self.app.borrow();
106        app.read_window(window, read)
107    }
108
109    fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
110    where
111        R: Send + 'static,
112    {
113        self.background_executor.spawn(future)
114    }
115
116    fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
117    where
118        G: Global,
119    {
120        let app = self.app.borrow();
121        app.read_global(callback)
122    }
123}
124
125impl TestAppContext {
126    /// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you.
127    pub fn build(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self {
128        let arc_dispatcher = Arc::new(dispatcher.clone());
129        let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
130        let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
131        let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
132        let asset_source = Arc::new(());
133        let http_client = http_client::FakeHttpClient::with_404_response();
134        let text_system = Arc::new(TextSystem::new(platform.text_system()));
135
136        let app = App::new_app(platform.clone(), asset_source, http_client);
137        app.borrow_mut().mode = GpuiMode::test();
138
139        Self {
140            app,
141            background_executor,
142            foreground_executor,
143            dispatcher,
144            test_platform: platform,
145            text_system,
146            fn_name,
147            on_quit: Rc::new(RefCell::new(Vec::default())),
148        }
149    }
150
151    /// Skip all drawing operations for the duration of this test.
152    pub fn skip_drawing(&mut self) {
153        self.app.borrow_mut().mode = GpuiMode::Test { skip_drawing: true };
154    }
155
156    /// Create a single TestAppContext, for non-multi-client tests
157    pub fn single() -> Self {
158        let dispatcher = TestDispatcher::new(0);
159        Self::build(dispatcher, None)
160    }
161
162    /// The name of the test function that created this `TestAppContext`
163    pub fn test_function_name(&self) -> Option<&'static str> {
164        self.fn_name
165    }
166
167    /// Checks whether there have been any new path prompts received by the platform.
168    pub fn did_prompt_for_new_path(&self) -> bool {
169        self.test_platform.did_prompt_for_new_path()
170    }
171
172    /// returns a new `TestAppContext` re-using the same executors to interleave tasks.
173    pub fn new_app(&self) -> TestAppContext {
174        Self::build(self.dispatcher.clone(), self.fn_name)
175    }
176
177    /// Called by the test helper to end the test.
178    /// public so the macro can call it.
179    pub fn quit(&self) {
180        self.on_quit.borrow_mut().drain(..).for_each(|f| f());
181        self.app.borrow_mut().shutdown();
182    }
183
184    /// Register cleanup to run when the test ends.
185    pub fn on_quit(&mut self, f: impl FnOnce() + 'static) {
186        self.on_quit.borrow_mut().push(Box::new(f));
187    }
188
189    /// Schedules all windows to be redrawn on the next effect cycle.
190    pub fn refresh(&mut self) -> Result<()> {
191        let mut app = self.app.borrow_mut();
192        app.refresh_windows();
193        Ok(())
194    }
195
196    /// Returns an executor (for running tasks in the background)
197    pub fn executor(&self) -> BackgroundExecutor {
198        self.background_executor.clone()
199    }
200
201    /// Returns an executor (for running tasks on the main thread)
202    pub fn foreground_executor(&self) -> &ForegroundExecutor {
203        &self.foreground_executor
204    }
205
206    /// Gives you an `&mut App` for the duration of the closure
207    pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
208        let mut cx = self.app.borrow_mut();
209        cx.update(f)
210    }
211
212    /// Gives you an `&App` for the duration of the closure
213    pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
214        let cx = self.app.borrow();
215        f(&cx)
216    }
217
218    /// Adds a new window. The Window will always be backed by a `TestWindow` which
219    /// can be retrieved with `self.test_window(handle)`
220    pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
221    where
222        F: FnOnce(&mut Window, &mut Context<V>) -> V,
223        V: 'static + Render,
224    {
225        let mut cx = self.app.borrow_mut();
226
227        // Some tests rely on the window size matching the bounds of the test display
228        let bounds = Bounds::maximized(None, &cx);
229        cx.open_window(
230            WindowOptions {
231                window_bounds: Some(WindowBounds::Windowed(bounds)),
232                ..Default::default()
233            },
234            |window, cx| cx.new(|cx| build_window(window, cx)),
235        )
236        .unwrap()
237    }
238
239    /// Opens a new window with a specific size.
240    ///
241    /// Unlike `add_window` which uses maximized bounds, this allows controlling
242    /// the window dimensions, which is important for layout-sensitive tests.
243    pub fn open_window<F, V>(
244        &mut self,
245        window_size: Size<Pixels>,
246        build_window: F,
247    ) -> WindowHandle<V>
248    where
249        F: FnOnce(&mut Window, &mut Context<V>) -> V,
250        V: 'static + Render,
251    {
252        let mut cx = self.app.borrow_mut();
253        cx.open_window(
254            WindowOptions {
255                window_bounds: Some(WindowBounds::Windowed(Bounds {
256                    origin: Point::default(),
257                    size: window_size,
258                })),
259                ..Default::default()
260            },
261            |window, cx| cx.new(|cx| build_window(window, cx)),
262        )
263        .unwrap()
264    }
265
266    /// Adds a new window with no content.
267    pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
268        let mut cx = self.app.borrow_mut();
269        let bounds = Bounds::maximized(None, &cx);
270        let window = cx
271            .open_window(
272                WindowOptions {
273                    window_bounds: Some(WindowBounds::Windowed(bounds)),
274                    ..Default::default()
275                },
276                |_, cx| cx.new(|_| Empty),
277            )
278            .unwrap();
279        drop(cx);
280        let cx = VisualTestContext::from_window(*window.deref(), self).into_mut();
281        cx.run_until_parked();
282        cx
283    }
284
285    /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
286    /// as a `Window` and `App` for the rest of the test. Typically you would shadow this context with
287    /// the returned one. `let (view, cx) = cx.add_window_view(...);`
288    pub fn add_window_view<F, V>(
289        &mut self,
290        build_root_view: F,
291    ) -> (Entity<V>, &mut VisualTestContext)
292    where
293        F: FnOnce(&mut Window, &mut Context<V>) -> V,
294        V: 'static + Render,
295    {
296        let mut cx = self.app.borrow_mut();
297        let bounds = Bounds::maximized(None, &cx);
298        let window = cx
299            .open_window(
300                WindowOptions {
301                    window_bounds: Some(WindowBounds::Windowed(bounds)),
302                    ..Default::default()
303                },
304                |window, cx| cx.new(|cx| build_root_view(window, cx)),
305            )
306            .unwrap();
307        drop(cx);
308        let view = window.root(self).unwrap();
309        let cx = VisualTestContext::from_window(*window.deref(), self).into_mut();
310        cx.run_until_parked();
311
312        // it might be nice to try and cleanup these at the end of each test.
313        (view, cx)
314    }
315
316    /// returns the TextSystem
317    pub fn text_system(&self) -> &Arc<TextSystem> {
318        &self.text_system
319    }
320
321    /// Simulates writing to the platform clipboard
322    pub fn write_to_clipboard(&self, item: ClipboardItem) {
323        self.test_platform.write_to_clipboard(item)
324    }
325
326    /// Simulates reading from the platform clipboard.
327    /// This will return the most recent value from `write_to_clipboard`.
328    pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
329        self.test_platform.read_from_clipboard()
330    }
331
332    /// Simulates choosing a File in the platform's "Open" dialog.
333    pub fn simulate_new_path_selection(
334        &self,
335        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
336    ) {
337        self.test_platform.simulate_new_path_selection(select_path);
338    }
339
340    /// Simulates responding to a `prompt_for_paths` ("Open") dialog.
341    pub fn simulate_path_prompt_response(
342        &self,
343        select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
344    ) {
345        self.test_platform
346            .simulate_path_prompt_response(select_paths);
347    }
348
349    /// Returns true if there's a path selection dialog pending.
350    pub fn did_prompt_for_paths(&self) -> bool {
351        self.test_platform.did_prompt_for_paths()
352    }
353
354    /// Simulates clicking a button in an platform-level alert dialog.
355    #[track_caller]
356    pub fn simulate_prompt_answer(&self, button: &str) {
357        self.test_platform.simulate_prompt_answer(button);
358    }
359
360    /// Returns true if there's an alert dialog open.
361    pub fn has_pending_prompt(&self) -> bool {
362        self.test_platform.has_pending_prompt()
363    }
364
365    /// Returns true if there's an alert dialog open.
366    pub fn pending_prompt(&self) -> Option<(String, String)> {
367        self.test_platform.pending_prompt()
368    }
369
370    /// All the urls that have been opened with cx.open_url() during this test.
371    pub fn opened_url(&self) -> Option<String> {
372        self.test_platform.opened_url.borrow().clone()
373    }
374
375    /// Returns the application identity configured during this test.
376    pub fn app_identity(&self) -> Option<(SharedString, SharedString)> {
377        self.test_platform.app_identity()
378    }
379
380    /// Returns all system notifications shown during this test, in order.
381    pub fn shown_system_notifications(&self) -> Vec<SystemNotification> {
382        self.test_platform.shown_system_notifications()
383    }
384
385    /// Returns the system notifications currently delivered by the test platform.
386    pub fn delivered_system_notifications(&self) -> Vec<SystemNotification> {
387        self.test_platform.delivered_system_notifications()
388    }
389
390    /// Returns the tags of all system notifications dismissed during this test, in order.
391    pub fn dismissed_system_notifications(&self) -> Vec<SharedString> {
392        self.test_platform.dismissed_system_notifications()
393    }
394
395    /// Simulates the user activating a system notification.
396    pub fn simulate_system_notification_response(&self, response: SystemNotificationResponse) {
397        self.test_platform
398            .simulate_system_notification_response(response);
399    }
400
401    /// Simulates the user resizing the window to the new size.
402    pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
403        self.test_window(window_handle).simulate_resize(size);
404    }
405
406    /// Returns true if there's an alert dialog open.
407    pub fn expect_restart(&self) -> oneshot::Receiver<Option<PathBuf>> {
408        let (tx, rx) = futures::channel::oneshot::channel();
409        self.test_platform.expect_restart.borrow_mut().replace(tx);
410        rx
411    }
412
413    /// Causes the given sources to be returned if the application queries for screen
414    /// capture sources.
415    pub fn set_screen_capture_sources(&self, sources: Vec<TestScreenCaptureSource>) {
416        self.test_platform.set_screen_capture_sources(sources);
417    }
418
419    /// Returns all windows open in the test.
420    pub fn windows(&self) -> Vec<AnyWindowHandle> {
421        self.app.borrow().windows()
422    }
423
424    /// Run the given task on the main thread.
425    #[track_caller]
426    pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
427    where
428        Fut: Future<Output = R> + 'static,
429        R: 'static,
430    {
431        self.foreground_executor.spawn(f(self.to_async()))
432    }
433
434    /// true if the given global is defined
435    pub fn has_global<G: Global>(&self) -> bool {
436        let app = self.app.borrow();
437        app.has_global::<G>()
438    }
439
440    /// runs the given closure with a reference to the global
441    /// panics if `has_global` would return false.
442    pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> R {
443        let app = self.app.borrow();
444        read(app.global(), &app)
445    }
446
447    /// runs the given closure with a reference to the global (if set)
448    pub fn try_read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Option<R> {
449        let lock = self.app.borrow();
450        Some(read(lock.try_global()?, &lock))
451    }
452
453    /// sets the global in this context.
454    pub fn set_global<G: Global>(&mut self, global: G) {
455        let mut lock = self.app.borrow_mut();
456        lock.update(|cx| cx.set_global(global))
457    }
458
459    /// updates the global in this context. (panics if `has_global` would return false)
460    pub fn update_global<G: Global, R>(&mut self, update: impl FnOnce(&mut G, &mut App) -> R) -> R {
461        let mut lock = self.app.borrow_mut();
462        lock.update(|cx| cx.update_global(update))
463    }
464
465    /// Returns an `AsyncApp` which can be used to run tasks that expect to be on a background
466    /// thread on the current thread in tests.
467    pub fn to_async(&self) -> AsyncApp {
468        AsyncApp {
469            app: Rc::downgrade(&self.app),
470            background_executor: self.background_executor.clone(),
471            foreground_executor: self.foreground_executor.clone(),
472        }
473    }
474
475    /// Wait until there are no more pending tasks.
476    pub fn run_until_parked(&self) {
477        self.dispatcher.run_until_parked();
478    }
479
480    /// Simulate dispatching an action to the currently focused node in the window.
481    pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
482    where
483        A: Action,
484    {
485        window
486            .update(self, |_, window, cx| {
487                window.dispatch_action(action.boxed_clone(), cx)
488            })
489            .unwrap();
490
491        self.background_executor.run_until_parked()
492    }
493
494    /// simulate_keystrokes takes a space-separated list of keys to type.
495    /// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
496    /// in Zed, this will run backspace on the current editor through the command palette.
497    /// This will also run the background executor until it's parked.
498    pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
499        for keystroke in keystrokes
500            .split(' ')
501            .map(Keystroke::parse)
502            .map(Result::unwrap)
503        {
504            self.dispatch_keystroke(window, keystroke);
505        }
506
507        self.background_executor.run_until_parked()
508    }
509
510    /// simulate_input takes a string of text to type.
511    /// cx.simulate_input("abc")
512    /// will type abc into your current editor
513    /// This will also run the background executor until it's parked.
514    pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
515        for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
516            self.dispatch_keystroke(window, keystroke);
517        }
518
519        self.background_executor.run_until_parked()
520    }
521
522    /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`)
523    pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
524        self.update_window(window, |_, window, cx| {
525            window.dispatch_keystroke(keystroke, cx)
526        })
527        .unwrap();
528    }
529
530    /// Returns the `TestWindow` backing the given handle.
531    pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
532        self.app
533            .borrow_mut()
534            .windows
535            .get_mut(window.id)
536            .unwrap()
537            .as_deref_mut()
538            .unwrap()
539            .platform_window
540            .as_test()
541            .unwrap()
542            .clone()
543    }
544
545    /// Returns a stream of notifications whenever the Entity is updated.
546    pub fn notifications<T: 'static>(
547        &mut self,
548        entity: &Entity<T>,
549    ) -> impl Stream<Item = ()> + use<T> {
550        let (tx, rx) = futures::channel::mpsc::unbounded();
551        self.update(|cx| {
552            cx.observe(entity, {
553                let tx = tx.clone();
554                move |_, _| {
555                    let _ = tx.unbounded_send(());
556                }
557            })
558            .detach();
559            cx.observe_release(entity, move |_, _| tx.close_channel())
560                .detach()
561        });
562        rx
563    }
564
565    /// Returns a stream of events emitted by the given Entity.
566    pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
567        &mut self,
568        entity: &Entity<T>,
569    ) -> futures::channel::mpsc::UnboundedReceiver<Evt>
570    where
571        Evt: 'static + Clone,
572    {
573        let (tx, rx) = futures::channel::mpsc::unbounded();
574        entity
575            .update(self, |_, cx: &mut Context<T>| {
576                cx.subscribe(entity, move |_entity, _handle, event, _cx| {
577                    let _ = tx.unbounded_send(event.clone());
578                })
579            })
580            .detach();
581        rx
582    }
583
584    /// Runs until the given condition becomes true. (Prefer `run_until_parked` if you
585    /// don't need to jump in at a specific time).
586    pub async fn condition<T: 'static>(
587        &mut self,
588        entity: &Entity<T>,
589        mut predicate: impl FnMut(&mut T, &mut Context<T>) -> bool,
590    ) {
591        let timer = self.executor().timer(Duration::from_secs(3));
592        let mut notifications = self.notifications(entity);
593
594        use futures::FutureExt as _;
595        use futures_concurrency::future::Race as _;
596
597        (
598            async {
599                loop {
600                    if entity.update(self, &mut predicate) {
601                        return Ok(());
602                    }
603
604                    if notifications.next().await.is_none() {
605                        bail!("entity dropped")
606                    }
607                }
608            },
609            timer.map(|_| Err(anyhow!("condition timed out"))),
610        )
611            .race()
612            .await
613            .unwrap();
614    }
615
616    /// Set a name for this App.
617    #[cfg(any(test, feature = "test-support"))]
618    pub fn set_name(&mut self, name: &'static str) {
619        self.update(|cx| cx.name = Some(name))
620    }
621}
622
623impl<T: 'static> Entity<T> {
624    /// Block until the next event is emitted by the entity, then return it.
625    pub fn next_event<Event>(&self, cx: &mut TestAppContext) -> impl Future<Output = Event>
626    where
627        Event: Send + Clone + 'static,
628        T: EventEmitter<Event>,
629    {
630        let (tx, mut rx) = oneshot::channel();
631        let mut tx = Some(tx);
632        let subscription = self.update(cx, |_, cx| {
633            cx.subscribe(self, move |_, _, event, _| {
634                if let Some(tx) = tx.take() {
635                    _ = tx.send(event.clone());
636                }
637            })
638        });
639
640        async move {
641            let event = rx.await.expect("no event emitted");
642            drop(subscription);
643            event
644        }
645    }
646}
647
648impl<V: 'static> Entity<V> {
649    /// Returns a future that resolves when the view is next updated.
650    pub fn next_notification(
651        &self,
652        advance_clock_by: Duration,
653        cx: &TestAppContext,
654    ) -> impl Future<Output = ()> {
655        use postage::prelude::{Sink as _, Stream as _};
656
657        let (mut tx, mut rx) = postage::mpsc::channel(1);
658        let subscription = cx.app.borrow_mut().observe(self, move |_, _| {
659            tx.try_send(()).ok();
660        });
661
662        cx.executor().advance_clock(advance_clock_by);
663
664        async move {
665            rx.recv()
666                .await
667                .expect("entity dropped while test was waiting for its next notification");
668            drop(subscription);
669        }
670    }
671}
672
673impl<V> Entity<V> {
674    /// Returns a future that resolves when the condition becomes true.
675    pub fn condition<Evt>(
676        &self,
677        cx: &TestAppContext,
678        mut predicate: impl FnMut(&V, &App) -> bool,
679    ) -> impl Future<Output = ()>
680    where
681        Evt: 'static,
682        V: EventEmitter<Evt>,
683    {
684        use postage::prelude::{Sink as _, Stream as _};
685
686        let (tx, mut rx) = postage::mpsc::channel(1024);
687
688        let mut cx = cx.app.borrow_mut();
689        let subscriptions = (
690            cx.observe(self, {
691                let mut tx = tx.clone();
692                move |_, _| {
693                    tx.blocking_send(()).ok();
694                }
695            }),
696            cx.subscribe(self, {
697                let mut tx = tx;
698                move |_, _: &Evt, _| {
699                    tx.blocking_send(()).ok();
700                }
701            }),
702        );
703
704        let cx = cx.this.upgrade().unwrap();
705        let handle = self.downgrade();
706
707        async move {
708            loop {
709                {
710                    let cx = cx.borrow();
711                    let cx = &*cx;
712                    if predicate(
713                        handle
714                            .upgrade()
715                            .expect("view dropped with pending condition")
716                            .read(cx),
717                        cx,
718                    ) {
719                        break;
720                    }
721                }
722
723                rx.recv()
724                    .await
725                    .expect("view dropped with pending condition");
726            }
727            drop(subscriptions);
728        }
729    }
730}
731
732use derive_more::{Deref, DerefMut};
733
734use super::{Context, Entity};
735#[derive(Deref, DerefMut, Clone)]
736/// A VisualTestContext is the test-equivalent of a `Window` and `App`. It allows you to
737/// run window-specific test code. It can be dereferenced to a `TextAppContext`.
738pub struct VisualTestContext {
739    #[deref]
740    #[deref_mut]
741    /// cx is the original TestAppContext (you can more easily access this using Deref)
742    pub cx: TestAppContext,
743    window: AnyWindowHandle,
744}
745
746impl VisualTestContext {
747    /// Provides a `Window` and `App` for the duration of the closure.
748    pub fn update<R>(&mut self, f: impl FnOnce(&mut Window, &mut App) -> R) -> R {
749        self.cx
750            .update_window(self.window, |_, window, cx| f(window, cx))
751            .unwrap()
752    }
753
754    /// Creates a new VisualTestContext. You would typically shadow the passed in
755    /// TestAppContext with this, as this is typically more useful.
756    /// `let cx = VisualTestContext::from_window(window, cx);`
757    pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self {
758        Self {
759            cx: cx.clone(),
760            window,
761        }
762    }
763
764    /// Wait until there are no more pending tasks.
765    pub fn run_until_parked(&self) {
766        self.cx.background_executor.run_until_parked();
767    }
768
769    /// Dispatch the action to the currently focused node.
770    pub fn dispatch_action<A>(&mut self, action: A)
771    where
772        A: Action,
773    {
774        self.cx.dispatch_action(self.window, action)
775    }
776
777    /// Read the title off the window (set by `Window#set_window_title`)
778    pub fn window_title(&mut self) -> Option<String> {
779        self.cx.test_window(self.window).0.lock().title.clone()
780    }
781
782    /// Read the document path off the window (set by `Window#set_document_path`)
783    pub fn document_path(&mut self) -> Option<std::path::PathBuf> {
784        self.cx
785            .test_window(self.window)
786            .0
787            .lock()
788            .document_path
789            .clone()
790    }
791
792    /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")`
793    /// Automatically runs until parked.
794    pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
795        self.cx.simulate_keystrokes(self.window, keystrokes)
796    }
797
798    /// Simulate typing text `cx.simulate_input("hello")`
799    /// Automatically runs until parked.
800    pub fn simulate_input(&mut self, input: &str) {
801        self.cx.simulate_input(self.window, input)
802    }
803
804    /// Simulate a mouse move event to the given point
805    pub fn simulate_mouse_move(
806        &mut self,
807        position: Point<Pixels>,
808        button: impl Into<Option<MouseButton>>,
809        modifiers: Modifiers,
810    ) {
811        self.simulate_event(MouseMoveEvent {
812            position,
813            modifiers,
814            pressed_button: button.into(),
815        })
816    }
817
818    /// Simulate a mouse down event to the given point
819    pub fn simulate_mouse_down(
820        &mut self,
821        position: Point<Pixels>,
822        button: MouseButton,
823        modifiers: Modifiers,
824    ) {
825        self.simulate_event(MouseDownEvent {
826            position,
827            modifiers,
828            button,
829            click_count: 1,
830            first_mouse: false,
831        })
832    }
833
834    /// Simulate a mouse up event to the given point
835    pub fn simulate_mouse_up(
836        &mut self,
837        position: Point<Pixels>,
838        button: MouseButton,
839        modifiers: Modifiers,
840    ) {
841        self.simulate_event(MouseUpEvent {
842            position,
843            modifiers,
844            button,
845            click_count: 1,
846        })
847    }
848
849    /// Simulate a primary mouse click at the given point
850    pub fn simulate_click(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
851        self.simulate_event(MouseDownEvent {
852            position,
853            modifiers,
854            button: MouseButton::Left,
855            click_count: 1,
856            first_mouse: false,
857        });
858        self.simulate_event(MouseUpEvent {
859            position,
860            modifiers,
861            button: MouseButton::Left,
862            click_count: 1,
863        });
864    }
865
866    /// Simulate a modifiers changed event
867    pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) {
868        self.simulate_event(ModifiersChangedEvent {
869            modifiers,
870            capslock: Capslock { on: false },
871        })
872    }
873
874    /// Simulate a capslock changed event
875    pub fn simulate_capslock_change(&mut self, on: bool) {
876        self.simulate_event(ModifiersChangedEvent {
877            modifiers: Modifiers::none(),
878            capslock: Capslock { on },
879        })
880    }
881
882    /// Simulates the user resizing the window to the new size.
883    pub fn simulate_resize(&self, size: Size<Pixels>) {
884        self.simulate_window_resize(self.window, size)
885    }
886
887    /// debug_bounds returns the bounds of the element with the given selector.
888    pub fn debug_bounds(&mut self, selector: &'static str) -> Option<Bounds<Pixels>> {
889        self.update(|window, _| window.rendered_frame.debug_bounds.get(selector).copied())
890    }
891
892    /// Draw an element to the window. Useful for simulating events or actions
893    pub fn draw<E>(
894        &mut self,
895        origin: Point<Pixels>,
896        space: impl Into<Size<AvailableSpace>>,
897        f: impl FnOnce(&mut Window, &mut App) -> E,
898    ) -> (E::RequestLayoutState, E::PrepaintState)
899    where
900        E: Element,
901    {
902        self.update(|window, cx| {
903            let arena_scope = ElementArenaScope::enter(&cx.element_arena);
904
905            window.invalidator.set_phase(DrawPhase::Prepaint);
906            let mut element = Drawable::new(f(window, cx));
907            element.layout_as_root(space.into(), window, cx);
908            window.with_absolute_element_offset(origin, |window| element.prepaint(window, cx));
909
910            window.invalidator.set_phase(DrawPhase::Paint);
911            let (request_layout_state, prepaint_state) = element.paint(window, cx);
912
913            window.invalidator.set_phase(DrawPhase::None);
914            window.refresh();
915
916            drop(element);
917            arena_scope.exit(&cx.element_arena).clear(cx);
918
919            (request_layout_state, prepaint_state)
920        })
921    }
922
923    /// Simulate an event from the platform, e.g. a ScrollWheelEvent
924    /// Make sure you've called [VisualTestContext::draw] first!
925    pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
926        self.test_window(self.window)
927            .simulate_input(event.to_platform_input());
928        self.background_executor.run_until_parked();
929    }
930
931    /// Simulates the user blurring the window.
932    pub fn deactivate_window(&mut self) {
933        if Some(self.window) == self.test_platform.active_window() {
934            self.test_platform.set_active_window(None)
935        }
936        self.background_executor.run_until_parked();
937    }
938
939    /// Simulates the user closing the window.
940    /// Returns true if the window was closed.
941    pub fn simulate_close(&mut self) -> bool {
942        let handler = self
943            .cx
944            .update_window(self.window, |_, window, _| {
945                window
946                    .platform_window
947                    .as_test()
948                    .unwrap()
949                    .0
950                    .lock()
951                    .should_close_handler
952                    .take()
953            })
954            .unwrap();
955        if let Some(mut handler) = handler {
956            let should_close = handler();
957            self.cx
958                .update_window(self.window, |_, window, _| {
959                    window.platform_window.on_should_close(handler);
960                })
961                .unwrap();
962            should_close
963        } else {
964            false
965        }
966    }
967
968    /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods).
969    /// This method internally retains the VisualTestContext until the end of the test.
970    pub fn into_mut(self) -> &'static mut Self {
971        let ptr = Box::into_raw(Box::new(self));
972        // safety: on_quit will be called after the test has finished.
973        // the executor will ensure that all tasks related to the test have stopped.
974        // so there is no way for cx to be accessed after on_quit is called.
975        // todo: This is unsound under stacked borrows (also tree borrows probably?)
976        // the mutable reference invalidates `ptr` which is later used in the closure
977        let cx = unsafe { &mut *ptr };
978        cx.on_quit(move || unsafe {
979            drop(Box::from_raw(ptr));
980        });
981        cx
982    }
983}
984
985impl AppContext for VisualTestContext {
986    fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
987        self.window
988            .update(&mut self.cx, |_, _, cx| cx.new(build_entity))
989            .expect("window was unexpectedly closed")
990    }
991
992    fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
993        self.cx.reserve_entity()
994    }
995
996    fn insert_entity<T: 'static>(
997        &mut self,
998        reservation: crate::Reservation<T>,
999        build_entity: impl FnOnce(&mut Context<T>) -> T,
1000    ) -> Entity<T> {
1001        self.window
1002            .update(&mut self.cx, |_, _, cx| {
1003                cx.insert_entity(reservation, build_entity)
1004            })
1005            .expect("window was unexpectedly closed")
1006    }
1007
1008    fn update_entity<T, R>(
1009        &mut self,
1010        handle: &Entity<T>,
1011        update: impl FnOnce(&mut T, &mut Context<T>) -> R,
1012    ) -> R
1013    where
1014        T: 'static,
1015    {
1016        self.cx.update_entity(handle, update)
1017    }
1018
1019    fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> super::GpuiBorrow<'a, T>
1020    where
1021        T: 'static,
1022    {
1023        self.cx.as_mut(handle)
1024    }
1025
1026    fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
1027    where
1028        T: 'static,
1029    {
1030        self.cx.read_entity(handle, read)
1031    }
1032
1033    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
1034    where
1035        F: FnOnce(AnyView, &mut Window, &mut App) -> T,
1036    {
1037        self.cx.update_window(window, f)
1038    }
1039
1040    fn with_window<R>(
1041        &mut self,
1042        entity_id: EntityId,
1043        f: impl FnOnce(&mut Window, &mut App) -> R,
1044    ) -> Option<R> {
1045        self.cx.with_window(entity_id, f)
1046    }
1047
1048    fn read_window<T, R>(
1049        &self,
1050        window: &WindowHandle<T>,
1051        read: impl FnOnce(Entity<T>, &App) -> R,
1052    ) -> Result<R>
1053    where
1054        T: 'static,
1055    {
1056        self.cx.read_window(window, read)
1057    }
1058
1059    fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
1060    where
1061        R: Send + 'static,
1062    {
1063        self.cx.background_spawn(future)
1064    }
1065
1066    fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
1067    where
1068        G: Global,
1069    {
1070        self.cx.read_global(callback)
1071    }
1072}
1073
1074impl VisualContext for VisualTestContext {
1075    type Result<T> = T;
1076
1077    /// Get the underlying window handle underlying this context.
1078    fn window_handle(&self) -> AnyWindowHandle {
1079        self.window
1080    }
1081
1082    fn new_window_entity<T: 'static>(
1083        &mut self,
1084        build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
1085    ) -> Entity<T> {
1086        self.window
1087            .update(&mut self.cx, |_, window, cx| {
1088                cx.new(|cx| build_entity(window, cx))
1089            })
1090            .expect("window was unexpectedly closed")
1091    }
1092
1093    fn update_window_entity<V: 'static, R>(
1094        &mut self,
1095        view: &Entity<V>,
1096        update: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R,
1097    ) -> R {
1098        let view = view.clone();
1099        self.cx
1100            .app
1101            .borrow_mut()
1102            .with_window(view.entity_id(), |window, app| {
1103                view.update(app, |v, cx| update(v, window, cx))
1104            })
1105            .expect("entity has no current window; use `update` instead of `update_in`")
1106    }
1107
1108    fn replace_root_view<V>(
1109        &mut self,
1110        build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
1111    ) -> Entity<V>
1112    where
1113        V: 'static + Render,
1114    {
1115        self.window
1116            .update(&mut self.cx, |_, window, cx| {
1117                window.replace_root(cx, build_view)
1118            })
1119            .expect("window was unexpectedly closed")
1120    }
1121
1122    fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) {
1123        self.window
1124            .update(&mut self.cx, |_, window, cx| {
1125                view.read(cx).focus_handle(cx).focus(window, cx)
1126            })
1127            .expect("window was unexpectedly closed")
1128    }
1129}
1130
1131impl AnyWindowHandle {
1132    /// Creates the given view in this window.
1133    pub fn build_entity<V: Render + 'static>(
1134        &self,
1135        cx: &mut TestAppContext,
1136        build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
1137    ) -> Entity<V> {
1138        self.update(cx, |_, window, cx| cx.new(|cx| build_view(window, cx)))
1139            .unwrap()
1140    }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145    use crate::{
1146        PathPromptOptions, SystemNotification, SystemNotificationAction,
1147        SystemNotificationResponse, TestAppContext,
1148    };
1149    use std::cell::RefCell;
1150    use std::path::PathBuf;
1151    use std::rc::Rc;
1152
1153    #[gpui::test]
1154    async fn test_system_notifications_require_identity_and_replace_matching_tags(
1155        cx: &mut TestAppContext,
1156    ) {
1157        cx.update(|cx| {
1158            cx.show_system_notification(SystemNotification {
1159                tag: "thread-1".into(),
1160                title: "Task started".into(),
1161                body: "Running tests".into(),
1162                actions: Vec::new(),
1163            });
1164        });
1165        assert!(cx.shown_system_notifications().is_empty());
1166        assert!(cx.delivered_system_notifications().is_empty());
1167
1168        cx.update(|cx| {
1169            cx.set_app_identity("com.example.tasks", "Tasks");
1170            cx.show_system_notification(SystemNotification {
1171                tag: "thread-1".into(),
1172                title: "Task started".into(),
1173                body: "Running tests".into(),
1174                actions: Vec::new(),
1175            });
1176            cx.show_system_notification(SystemNotification {
1177                tag: "thread-1".into(),
1178                title: "Task finished".into(),
1179                body: "All tests passed".into(),
1180                actions: vec![SystemNotificationAction {
1181                    id: "open".into(),
1182                    label: "Open".into(),
1183                }],
1184            });
1185        });
1186
1187        assert_eq!(
1188            cx.app_identity(),
1189            Some(("com.example.tasks".into(), "Tasks".into()))
1190        );
1191        assert_eq!(cx.shown_system_notifications().len(), 2);
1192        assert_eq!(
1193            cx.delivered_system_notifications(),
1194            [SystemNotification {
1195                tag: "thread-1".into(),
1196                title: "Task finished".into(),
1197                body: "All tests passed".into(),
1198                actions: vec![SystemNotificationAction {
1199                    id: "open".into(),
1200                    label: "Open".into(),
1201                }],
1202            }]
1203        );
1204
1205        cx.update(|cx| cx.dismiss_system_notification("thread-1"));
1206        assert!(cx.delivered_system_notifications().is_empty());
1207        assert_eq!(cx.dismissed_system_notifications(), ["thread-1"]);
1208    }
1209
1210    #[gpui::test]
1211    async fn test_system_notification_body_and_action_responses(cx: &mut TestAppContext) {
1212        let responses = Rc::new(RefCell::new(Vec::new()));
1213        cx.update(|cx| {
1214            cx.on_system_notification_response({
1215                let responses = responses.clone();
1216                move |response, _cx| responses.borrow_mut().push(response)
1217            });
1218        });
1219
1220        cx.simulate_system_notification_response(SystemNotificationResponse {
1221            tag: "thread-1".into(),
1222            action_id: None,
1223        });
1224        cx.simulate_system_notification_response(SystemNotificationResponse {
1225            tag: "thread-1".into(),
1226            action_id: Some("default".into()),
1227        });
1228
1229        assert_eq!(
1230            responses.borrow().as_slice(),
1231            &[
1232                SystemNotificationResponse {
1233                    tag: "thread-1".into(),
1234                    action_id: None,
1235                },
1236                SystemNotificationResponse {
1237                    tag: "thread-1".into(),
1238                    action_id: Some("default".into()),
1239                },
1240            ]
1241        );
1242    }
1243
1244    #[gpui::test]
1245    async fn test_system_notification_response_handler_can_be_replaced(cx: &mut TestAppContext) {
1246        let first_responses = Rc::new(RefCell::new(Vec::new()));
1247        let second_responses = Rc::new(RefCell::new(Vec::new()));
1248        cx.update(|cx| {
1249            cx.on_system_notification_response({
1250                let first_responses = first_responses.clone();
1251                move |response, _cx| first_responses.borrow_mut().push(response)
1252            });
1253            cx.on_system_notification_response({
1254                let second_responses = second_responses.clone();
1255                move |response, _cx| second_responses.borrow_mut().push(response)
1256            });
1257        });
1258
1259        let response = SystemNotificationResponse {
1260            tag: "thread-1".into(),
1261            action_id: None,
1262        };
1263        cx.simulate_system_notification_response(response.clone());
1264
1265        assert!(first_responses.borrow().is_empty());
1266        assert_eq!(second_responses.borrow().as_slice(), &[response]);
1267    }
1268
1269    #[gpui::test]
1270    async fn test_system_notification_response_handler_can_reenter_app(cx: &mut TestAppContext) {
1271        cx.update(|cx| {
1272            cx.set_app_identity("com.example.tasks", "Tasks");
1273            cx.show_system_notification(SystemNotification {
1274                tag: "thread-1".into(),
1275                title: "Task finished".into(),
1276                body: "All tests passed".into(),
1277                actions: Vec::new(),
1278            });
1279            cx.on_system_notification_response(|response, cx| {
1280                cx.dismiss_system_notification(&response.tag);
1281            });
1282        });
1283
1284        cx.simulate_system_notification_response(SystemNotificationResponse {
1285            tag: "thread-1".into(),
1286            action_id: None,
1287        });
1288
1289        assert!(cx.delivered_system_notifications().is_empty());
1290        assert_eq!(cx.dismissed_system_notifications(), ["thread-1"]);
1291    }
1292
1293    #[gpui::test]
1294    async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) {
1295        assert!(!cx.did_prompt_for_paths());
1296
1297        let receiver = cx.update(|cx| {
1298            cx.prompt_for_paths(PathPromptOptions {
1299                files: false,
1300                directories: true,
1301                multiple: true,
1302                prompt: None,
1303            })
1304        });
1305        assert!(cx.did_prompt_for_paths());
1306
1307        let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")];
1308        cx.simulate_path_prompt_response({
1309            let selected = selected.clone();
1310            move |options| {
1311                assert!(options.multiple);
1312                Some(selected)
1313            }
1314        });
1315        assert!(!cx.did_prompt_for_paths());
1316
1317        let response = receiver.await.unwrap().unwrap();
1318        assert_eq!(response, Some(selected));
1319    }
1320
1321    #[gpui::test]
1322    async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) {
1323        let receiver = cx.update(|cx| {
1324            cx.prompt_for_paths(PathPromptOptions {
1325                files: true,
1326                directories: false,
1327                multiple: false,
1328                prompt: None,
1329            })
1330        });
1331
1332        cx.simulate_path_prompt_response(|_options| None);
1333
1334        let response = receiver.await.unwrap().unwrap();
1335        assert_eq!(response, None);
1336    }
1337}
1338
Served at tenant.openagents/omega Member data and write actions are omitted.