test_app_context.rs

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