test_app_context.rs

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