test_app_context.rs

  1use crate::{
  2    executor,
  3    geometry::vector::Vector2F,
  4    keymap_matcher::{Binding, Keystroke},
  5    platform,
  6    platform::{Event, InputHandler, KeyDownEvent, Platform},
  7    Action, AnyWindowHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
  8    Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
  9    WeakHandle, WindowContext, WindowHandle,
 10};
 11use collections::BTreeMap;
 12use futures::Future;
 13use itertools::Itertools;
 14use parking_lot::{Mutex, RwLock};
 15use smallvec::SmallVec;
 16use smol::stream::StreamExt;
 17use std::{
 18    any::Any,
 19    cell::RefCell,
 20    mem,
 21    path::PathBuf,
 22    rc::Rc,
 23    sync::{
 24        atomic::{AtomicUsize, Ordering},
 25        Arc,
 26    },
 27    time::Duration,
 28};
 29
 30use super::{
 31    ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
 32};
 33
 34#[derive(Clone)]
 35pub struct TestAppContext {
 36    cx: Rc<RefCell<AppContext>>,
 37    foreground_platform: Rc<platform::test::ForegroundPlatform>,
 38    condition_duration: Option<Duration>,
 39    pub function_name: String,
 40    assertion_context: AssertionContextManager,
 41}
 42
 43impl TestAppContext {
 44    pub fn new(
 45        foreground_platform: Rc<platform::test::ForegroundPlatform>,
 46        platform: Arc<dyn Platform>,
 47        foreground: Rc<executor::Foreground>,
 48        background: Arc<executor::Background>,
 49        font_cache: Arc<FontCache>,
 50        leak_detector: Arc<Mutex<LeakDetector>>,
 51        first_entity_id: usize,
 52        function_name: String,
 53    ) -> Self {
 54        let mut cx = AppContext::new(
 55            foreground,
 56            background,
 57            platform,
 58            foreground_platform.clone(),
 59            font_cache,
 60            RefCounts::new(leak_detector),
 61            (),
 62        );
 63        cx.next_id = first_entity_id;
 64        let cx = TestAppContext {
 65            cx: Rc::new(RefCell::new(cx)),
 66            foreground_platform,
 67            condition_duration: None,
 68            function_name,
 69            assertion_context: AssertionContextManager::new(),
 70        };
 71        cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
 72        cx
 73    }
 74
 75    pub fn dispatch_action<A: Action>(&mut self, window: AnyWindowHandle, action: A) {
 76        self.update_window(window, |window| {
 77            window.dispatch_action(window.focused_view_id(), &action);
 78        })
 79        .expect("window not found");
 80    }
 81
 82    pub fn available_actions(
 83        &self,
 84        window: AnyWindowHandle,
 85        view_id: usize,
 86    ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
 87        self.read_window(window, |cx| cx.available_actions(view_id))
 88            .unwrap_or_default()
 89    }
 90
 91    pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
 92        self.update(|cx| cx.dispatch_global_action_any(&action));
 93    }
 94
 95    pub fn dispatch_keystroke(
 96        &mut self,
 97        window: AnyWindowHandle,
 98        keystroke: Keystroke,
 99        is_held: bool,
100    ) {
101        let handled = window.update(self, |cx| {
102            if cx.dispatch_keystroke(&keystroke) {
103                return true;
104            }
105
106            if cx.dispatch_event(
107                Event::KeyDown(KeyDownEvent {
108                    keystroke: keystroke.clone(),
109                    is_held,
110                }),
111                false,
112            ) {
113                return true;
114            }
115
116            false
117        });
118
119        if !handled && !keystroke.cmd && !keystroke.ctrl {
120            WindowInputHandler {
121                app: self.cx.clone(),
122                window,
123            }
124            .replace_text_in_range(None, &keystroke.key)
125        }
126    }
127
128    pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
129        &self,
130        window: AnyWindowHandle,
131        callback: F,
132    ) -> Option<T> {
133        self.cx.borrow().read_window(window, callback)
134    }
135
136    pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
137        &mut self,
138        window: AnyWindowHandle,
139        callback: F,
140    ) -> Option<T> {
141        self.cx.borrow_mut().update_window(window, callback)
142    }
143
144    pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
145    where
146        T: Entity,
147        F: FnOnce(&mut ModelContext<T>) -> T,
148    {
149        self.cx.borrow_mut().add_model(build_model)
150    }
151
152    pub fn add_window<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
153    where
154        V: View,
155        F: FnOnce(&mut ViewContext<V>) -> V,
156    {
157        let window = self
158            .cx
159            .borrow_mut()
160            .add_window(Default::default(), build_root_view);
161        window.simulate_activation(self);
162        window
163    }
164
165    pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription
166    where
167        E: Any,
168        F: 'static + FnMut(&mut AppContext),
169    {
170        self.cx.borrow_mut().observe_global::<E, F>(callback)
171    }
172
173    pub fn set_global<T: 'static>(&mut self, state: T) {
174        self.cx.borrow_mut().set_global(state);
175    }
176
177    pub fn subscribe_global<E, F>(&mut self, callback: F) -> Subscription
178    where
179        E: Any,
180        F: 'static + FnMut(&E, &mut AppContext),
181    {
182        self.cx.borrow_mut().subscribe_global(callback)
183    }
184
185    pub fn windows(&self) -> Vec<AnyWindowHandle> {
186        self.cx.borrow().windows().collect()
187    }
188
189    pub fn remove_all_windows(&mut self) {
190        self.update(|cx| cx.windows.clear());
191    }
192
193    pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
194        callback(&*self.cx.borrow())
195    }
196
197    pub fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, callback: F) -> T {
198        let mut state = self.cx.borrow_mut();
199        // Don't increment pending flushes in order for effects to be flushed before the callback
200        // completes, which is helpful in tests.
201        let result = callback(&mut *state);
202        // Flush effects after the callback just in case there are any. This can happen in edge
203        // cases such as the closure dropping handles.
204        state.flush_effects();
205        result
206    }
207
208    pub fn to_async(&self) -> AsyncAppContext {
209        AsyncAppContext(self.cx.clone())
210    }
211
212    pub fn font_cache(&self) -> Arc<FontCache> {
213        self.cx.borrow().font_cache.clone()
214    }
215
216    pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
217        self.foreground_platform.clone()
218    }
219
220    pub fn platform(&self) -> Arc<dyn platform::Platform> {
221        self.cx.borrow().platform.clone()
222    }
223
224    pub fn foreground(&self) -> Rc<executor::Foreground> {
225        self.cx.borrow().foreground().clone()
226    }
227
228    pub fn background(&self) -> Arc<executor::Background> {
229        self.cx.borrow().background().clone()
230    }
231
232    pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
233    where
234        F: FnOnce(AsyncAppContext) -> Fut,
235        Fut: 'static + Future<Output = T>,
236        T: 'static,
237    {
238        let foreground = self.foreground();
239        let future = f(self.to_async());
240        let cx = self.to_async();
241        foreground.spawn(async move {
242            let result = future.await;
243            cx.0.borrow_mut().flush_effects();
244            result
245        })
246    }
247
248    pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
249        self.foreground_platform.simulate_new_path_selection(result);
250    }
251
252    pub fn did_prompt_for_new_path(&self) -> bool {
253        self.foreground_platform.as_ref().did_prompt_for_new_path()
254    }
255
256    pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
257        self.cx.borrow().leak_detector()
258    }
259
260    pub fn assert_dropped(&self, handle: impl WeakHandle) {
261        self.cx
262            .borrow()
263            .leak_detector()
264            .lock()
265            .assert_dropped(handle.id())
266    }
267
268    /// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
269    /// where the stray handles were created.
270    pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
271        let weak = handle.downgrade();
272        self.update(|_| drop(handle));
273        self.assert_dropped(weak);
274    }
275
276    pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
277        self.condition_duration = duration;
278    }
279
280    pub fn condition_duration(&self) -> Duration {
281        self.condition_duration.unwrap_or_else(|| {
282            if std::env::var("CI").is_ok() {
283                Duration::from_secs(2)
284            } else {
285                Duration::from_millis(500)
286            }
287        })
288    }
289
290    pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
291        self.update(|cx| {
292            let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
293            let expected_content = expected_content.map(|content| content.to_owned());
294            assert_eq!(actual_content, expected_content);
295        })
296    }
297
298    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
299        self.assertion_context.add_context(context)
300    }
301
302    pub fn assertion_context(&self) -> String {
303        self.assertion_context.context()
304    }
305}
306
307impl BorrowAppContext for TestAppContext {
308    fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
309        self.cx.borrow().read_with(f)
310    }
311
312    fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
313        self.cx.borrow_mut().update(f)
314    }
315}
316
317impl BorrowWindowContext for TestAppContext {
318    type Result<T> = T;
319
320    fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
321        self.cx
322            .borrow()
323            .read_window(window, f)
324            .expect("window was closed")
325    }
326
327    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
328    where
329        F: FnOnce(&WindowContext) -> Option<T>,
330    {
331        BorrowWindowContext::read_window(self, window, f)
332    }
333
334    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
335        &mut self,
336        window: AnyWindowHandle,
337        f: F,
338    ) -> T {
339        self.cx
340            .borrow_mut()
341            .update_window(window, f)
342            .expect("window was closed")
343    }
344
345    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
346    where
347        F: FnOnce(&mut WindowContext) -> Option<T>,
348    {
349        BorrowWindowContext::update_window(self, window, f)
350    }
351}
352
353impl<T: Entity> ModelHandle<T> {
354    pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
355        let (tx, mut rx) = futures::channel::mpsc::unbounded();
356        let mut cx = cx.cx.borrow_mut();
357        let subscription = cx.observe(self, move |_, _| {
358            tx.unbounded_send(()).ok();
359        });
360
361        let duration = if std::env::var("CI").is_ok() {
362            Duration::from_secs(5)
363        } else {
364            Duration::from_secs(1)
365        };
366
367        let executor = cx.background().clone();
368        async move {
369            executor.start_waiting();
370            let notification = crate::util::timeout(duration, rx.next())
371                .await
372                .expect("next notification timed out");
373            drop(subscription);
374            notification.expect("model dropped while test was waiting for its next notification")
375        }
376    }
377
378    pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
379    where
380        T::Event: Clone,
381    {
382        let (tx, mut rx) = futures::channel::mpsc::unbounded();
383        let mut cx = cx.cx.borrow_mut();
384        let subscription = cx.subscribe(self, move |_, event, _| {
385            tx.unbounded_send(event.clone()).ok();
386        });
387
388        let duration = if std::env::var("CI").is_ok() {
389            Duration::from_secs(5)
390        } else {
391            Duration::from_secs(1)
392        };
393
394        cx.foreground.start_waiting();
395        async move {
396            let event = crate::util::timeout(duration, rx.next())
397                .await
398                .expect("next event timed out");
399            drop(subscription);
400            event.expect("model dropped while test was waiting for its next event")
401        }
402    }
403
404    pub fn condition(
405        &self,
406        cx: &TestAppContext,
407        mut predicate: impl FnMut(&T, &AppContext) -> bool,
408    ) -> impl Future<Output = ()> {
409        let (tx, mut rx) = futures::channel::mpsc::unbounded();
410
411        let mut cx = cx.cx.borrow_mut();
412        let subscriptions = (
413            cx.observe(self, {
414                let tx = tx.clone();
415                move |_, _| {
416                    tx.unbounded_send(()).ok();
417                }
418            }),
419            cx.subscribe(self, {
420                move |_, _, _| {
421                    tx.unbounded_send(()).ok();
422                }
423            }),
424        );
425
426        let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
427        let handle = self.downgrade();
428        let duration = if std::env::var("CI").is_ok() {
429            Duration::from_secs(5)
430        } else {
431            Duration::from_secs(1)
432        };
433
434        async move {
435            crate::util::timeout(duration, async move {
436                loop {
437                    {
438                        let cx = cx.borrow();
439                        let cx = &*cx;
440                        if predicate(
441                            handle
442                                .upgrade(cx)
443                                .expect("model dropped with pending condition")
444                                .read(cx),
445                            cx,
446                        ) {
447                            break;
448                        }
449                    }
450
451                    cx.borrow().foreground().start_waiting();
452                    rx.next()
453                        .await
454                        .expect("model dropped with pending condition");
455                    cx.borrow().foreground().finish_waiting();
456                }
457            })
458            .await
459            .expect("condition timed out");
460            drop(subscriptions);
461        }
462    }
463}
464
465impl AnyWindowHandle {
466    pub fn has_pending_prompt(&self, cx: &mut TestAppContext) -> bool {
467        let window = self.platform_window_mut(cx);
468        let prompts = window.pending_prompts.borrow_mut();
469        !prompts.is_empty()
470    }
471
472    pub fn current_title(&self, cx: &mut TestAppContext) -> Option<String> {
473        self.platform_window_mut(cx).title.clone()
474    }
475
476    pub fn simulate_close(&self, cx: &mut TestAppContext) -> bool {
477        let handler = self.platform_window_mut(cx).should_close_handler.take();
478        if let Some(mut handler) = handler {
479            let should_close = handler();
480            self.platform_window_mut(cx).should_close_handler = Some(handler);
481            should_close
482        } else {
483            false
484        }
485    }
486
487    pub fn simulate_resize(&self, size: Vector2F, cx: &mut TestAppContext) {
488        let mut window = self.platform_window_mut(cx);
489        window.size = size;
490        let mut handlers = mem::take(&mut window.resize_handlers);
491        drop(window);
492        for handler in &mut handlers {
493            handler();
494        }
495        self.platform_window_mut(cx).resize_handlers = handlers;
496    }
497
498    pub fn is_edited(&self, cx: &mut TestAppContext) -> bool {
499        self.platform_window_mut(cx).edited
500    }
501
502    pub fn simulate_prompt_answer(&self, answer: usize, cx: &mut TestAppContext) {
503        use postage::prelude::Sink as _;
504
505        let mut done_tx = self
506            .platform_window_mut(cx)
507            .pending_prompts
508            .borrow_mut()
509            .pop_front()
510            .expect("prompt was not called");
511        done_tx.try_send(answer).ok();
512    }
513
514    fn platform_window_mut<'a>(
515        &self,
516        cx: &'a mut TestAppContext,
517    ) -> std::cell::RefMut<'a, platform::test::Window> {
518        std::cell::RefMut::map(cx.cx.borrow_mut(), |state| {
519            let window = state.windows.get_mut(&self).unwrap();
520            let test_window = window
521                .platform_window
522                .as_any_mut()
523                .downcast_mut::<platform::test::Window>()
524                .unwrap();
525            test_window
526        })
527    }
528}
529
530impl<T: View> ViewHandle<T> {
531    pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
532        use postage::prelude::{Sink as _, Stream as _};
533
534        let (mut tx, mut rx) = postage::mpsc::channel(1);
535        let mut cx = cx.cx.borrow_mut();
536        let subscription = cx.observe(self, move |_, _| {
537            tx.try_send(()).ok();
538        });
539
540        let duration = if std::env::var("CI").is_ok() {
541            Duration::from_secs(5)
542        } else {
543            Duration::from_secs(1)
544        };
545
546        async move {
547            let notification = crate::util::timeout(duration, rx.recv())
548                .await
549                .expect("next notification timed out");
550            drop(subscription);
551            notification.expect("model dropped while test was waiting for its next notification")
552        }
553    }
554
555    pub fn condition(
556        &self,
557        cx: &TestAppContext,
558        mut predicate: impl FnMut(&T, &AppContext) -> bool,
559    ) -> impl Future<Output = ()> {
560        use postage::prelude::{Sink as _, Stream as _};
561
562        let (tx, mut rx) = postage::mpsc::channel(1024);
563        let timeout_duration = cx.condition_duration();
564
565        let mut cx = cx.cx.borrow_mut();
566        let subscriptions = (
567            cx.observe(self, {
568                let mut tx = tx.clone();
569                move |_, _| {
570                    tx.blocking_send(()).ok();
571                }
572            }),
573            cx.subscribe(self, {
574                let mut tx = tx.clone();
575                move |_, _, _| {
576                    tx.blocking_send(()).ok();
577                }
578            }),
579        );
580
581        let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
582        let handle = self.downgrade();
583
584        async move {
585            crate::util::timeout(timeout_duration, async move {
586                loop {
587                    {
588                        let cx = cx.borrow();
589                        let cx = &*cx;
590                        if predicate(
591                            handle
592                                .upgrade(cx)
593                                .expect("view dropped with pending condition")
594                                .read(cx),
595                            cx,
596                        ) {
597                            break;
598                        }
599                    }
600
601                    cx.borrow().foreground().start_waiting();
602                    rx.recv()
603                        .await
604                        .expect("view dropped with pending condition");
605                    cx.borrow().foreground().finish_waiting();
606                }
607            })
608            .await
609            .expect("condition timed out");
610            drop(subscriptions);
611        }
612    }
613}
614
615/// Tracks string context to be printed when assertions fail.
616/// Often this is done by storing a context string in the manager and returning the handle.
617#[derive(Clone)]
618pub struct AssertionContextManager {
619    id: Arc<AtomicUsize>,
620    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
621}
622
623impl AssertionContextManager {
624    pub fn new() -> Self {
625        Self {
626            id: Arc::new(AtomicUsize::new(0)),
627            contexts: Arc::new(RwLock::new(BTreeMap::new())),
628        }
629    }
630
631    pub fn add_context(&self, context: String) -> ContextHandle {
632        let id = self.id.fetch_add(1, Ordering::Relaxed);
633        let mut contexts = self.contexts.write();
634        contexts.insert(id, context);
635        ContextHandle {
636            id,
637            manager: self.clone(),
638        }
639    }
640
641    pub fn context(&self) -> String {
642        let contexts = self.contexts.read();
643        format!("\n{}\n", contexts.values().join("\n"))
644    }
645}
646
647/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
648/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
649/// the state that was set initially for the failure can be printed in the error message
650pub struct ContextHandle {
651    id: usize,
652    manager: AssertionContextManager,
653}
654
655impl Drop for ContextHandle {
656    fn drop(&mut self) {
657        let mut contexts = self.manager.contexts.write();
658        contexts.remove(&self.id);
659    }
660}