test_app_context.rs

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