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, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle,
  8    ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle,
  9    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.dispatch_action(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, window_id: usize, build_view: F) -> ViewHandle<T>
157    where
158        T: View,
159        F: FnOnce(&mut ViewContext<T>) -> T,
160    {
161        self.update_window(window_id, |cx| cx.add_view(build_view))
162            .expect("window not found")
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 window_ids(&self) -> Vec<usize> {
186        self.cx.borrow().windows.keys().copied().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 simulate_prompt_answer(&self, window_id: usize, answer: usize) {
257        use postage::prelude::Sink as _;
258
259        let mut done_tx = self
260            .platform_window_mut(window_id)
261            .pending_prompts
262            .borrow_mut()
263            .pop_front()
264            .expect("prompt was not called");
265        let _ = done_tx.try_send(answer);
266    }
267
268    pub fn has_pending_prompt(&self, window_id: usize) -> bool {
269        let window = self.platform_window_mut(window_id);
270        let prompts = window.pending_prompts.borrow_mut();
271        !prompts.is_empty()
272    }
273
274    pub fn current_window_title(&self, window_id: usize) -> Option<String> {
275        self.platform_window_mut(window_id).title.clone()
276    }
277
278    pub fn simulate_window_close(&self, window_id: usize) -> bool {
279        let handler = self
280            .platform_window_mut(window_id)
281            .should_close_handler
282            .take();
283        if let Some(mut handler) = handler {
284            let should_close = handler();
285            self.platform_window_mut(window_id).should_close_handler = Some(handler);
286            should_close
287        } else {
288            false
289        }
290    }
291
292    pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
293        let mut window = self.platform_window_mut(window_id);
294        window.size = size;
295        let mut handlers = mem::take(&mut window.resize_handlers);
296        drop(window);
297        for handler in &mut handlers {
298            handler();
299        }
300        self.platform_window_mut(window_id).resize_handlers = handlers;
301    }
302
303    pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
304        self.cx.borrow_mut().update(|cx| {
305            let other_window_ids = cx
306                .windows
307                .keys()
308                .filter(|window_id| Some(**window_id) != to_activate)
309                .copied()
310                .collect::<Vec<_>>();
311
312            for window_id in other_window_ids {
313                cx.window_changed_active_status(window_id, false)
314            }
315
316            if let Some(to_activate) = to_activate {
317                cx.window_changed_active_status(to_activate, true)
318            }
319        });
320    }
321
322    pub fn is_window_edited(&self, window_id: usize) -> bool {
323        self.platform_window_mut(window_id).edited
324    }
325
326    pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
327        self.cx.borrow().leak_detector()
328    }
329
330    pub fn assert_dropped(&self, handle: impl WeakHandle) {
331        self.cx
332            .borrow()
333            .leak_detector()
334            .lock()
335            .assert_dropped(handle.id())
336    }
337
338    /// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
339    /// where the stray handles were created.
340    pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
341        let weak = handle.downgrade();
342        self.update(|_| drop(handle));
343        self.assert_dropped(weak);
344    }
345
346    fn platform_window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
347        std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
348            let window = state.windows.get_mut(&window_id).unwrap();
349            let test_window = window
350                .platform_window
351                .as_any_mut()
352                .downcast_mut::<platform::test::Window>()
353                .unwrap();
354            test_window
355        })
356    }
357
358    pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
359        self.condition_duration = duration;
360    }
361
362    pub fn condition_duration(&self) -> Duration {
363        self.condition_duration.unwrap_or_else(|| {
364            if std::env::var("CI").is_ok() {
365                Duration::from_secs(2)
366            } else {
367                Duration::from_millis(500)
368            }
369        })
370    }
371
372    pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
373        self.update(|cx| {
374            let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
375            let expected_content = expected_content.map(|content| content.to_owned());
376            assert_eq!(actual_content, expected_content);
377        })
378    }
379
380    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
381        self.assertion_context.add_context(context)
382    }
383
384    pub fn assertion_context(&self) -> String {
385        self.assertion_context.context()
386    }
387}
388
389impl BorrowAppContext for TestAppContext {
390    fn read_with<T, F: FnOnce(&AppContext) -> T>(&self, f: F) -> T {
391        self.cx.borrow().read_with(f)
392    }
393
394    fn update<T, F: FnOnce(&mut AppContext) -> T>(&mut self, f: F) -> T {
395        self.cx.borrow_mut().update(f)
396    }
397}
398
399impl BorrowWindowContext for TestAppContext {
400    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
401        self.cx
402            .borrow()
403            .read_window(window_id, f)
404            .expect("window was closed")
405    }
406
407    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
408        self.cx
409            .borrow_mut()
410            .update_window(window_id, f)
411            .expect("window was closed")
412    }
413}
414
415impl<T: Entity> ModelHandle<T> {
416    pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
417        let (tx, mut rx) = futures::channel::mpsc::unbounded();
418        let mut cx = cx.cx.borrow_mut();
419        let subscription = cx.observe(self, move |_, _| {
420            tx.unbounded_send(()).ok();
421        });
422
423        let duration = if std::env::var("CI").is_ok() {
424            Duration::from_secs(5)
425        } else {
426            Duration::from_secs(1)
427        };
428
429        async move {
430            let notification = crate::util::timeout(duration, rx.next())
431                .await
432                .expect("next notification timed out");
433            drop(subscription);
434            notification.expect("model dropped while test was waiting for its next notification")
435        }
436    }
437
438    pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
439    where
440        T::Event: Clone,
441    {
442        let (tx, mut rx) = futures::channel::mpsc::unbounded();
443        let mut cx = cx.cx.borrow_mut();
444        let subscription = cx.subscribe(self, move |_, event, _| {
445            tx.unbounded_send(event.clone()).ok();
446        });
447
448        let duration = if std::env::var("CI").is_ok() {
449            Duration::from_secs(5)
450        } else {
451            Duration::from_secs(1)
452        };
453
454        cx.foreground.start_waiting();
455        async move {
456            let event = crate::util::timeout(duration, rx.next())
457                .await
458                .expect("next event timed out");
459            drop(subscription);
460            event.expect("model dropped while test was waiting for its next event")
461        }
462    }
463
464    pub fn condition(
465        &self,
466        cx: &TestAppContext,
467        mut predicate: impl FnMut(&T, &AppContext) -> bool,
468    ) -> impl Future<Output = ()> {
469        let (tx, mut rx) = futures::channel::mpsc::unbounded();
470
471        let mut cx = cx.cx.borrow_mut();
472        let subscriptions = (
473            cx.observe(self, {
474                let tx = tx.clone();
475                move |_, _| {
476                    tx.unbounded_send(()).ok();
477                }
478            }),
479            cx.subscribe(self, {
480                move |_, _, _| {
481                    tx.unbounded_send(()).ok();
482                }
483            }),
484        );
485
486        let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
487        let handle = self.downgrade();
488        let duration = if std::env::var("CI").is_ok() {
489            Duration::from_secs(5)
490        } else {
491            Duration::from_secs(1)
492        };
493
494        async move {
495            crate::util::timeout(duration, async move {
496                loop {
497                    {
498                        let cx = cx.borrow();
499                        let cx = &*cx;
500                        if predicate(
501                            handle
502                                .upgrade(cx)
503                                .expect("model dropped with pending condition")
504                                .read(cx),
505                            cx,
506                        ) {
507                            break;
508                        }
509                    }
510
511                    cx.borrow().foreground().start_waiting();
512                    rx.next()
513                        .await
514                        .expect("model dropped with pending condition");
515                    cx.borrow().foreground().finish_waiting();
516                }
517            })
518            .await
519            .expect("condition timed out");
520            drop(subscriptions);
521        }
522    }
523}
524
525impl<T: View> ViewHandle<T> {
526    pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
527        use postage::prelude::{Sink as _, Stream as _};
528
529        let (mut tx, mut rx) = postage::mpsc::channel(1);
530        let mut cx = cx.cx.borrow_mut();
531        let subscription = cx.observe(self, move |_, _| {
532            tx.try_send(()).ok();
533        });
534
535        let duration = if std::env::var("CI").is_ok() {
536            Duration::from_secs(5)
537        } else {
538            Duration::from_secs(1)
539        };
540
541        async move {
542            let notification = crate::util::timeout(duration, rx.recv())
543                .await
544                .expect("next notification timed out");
545            drop(subscription);
546            notification.expect("model dropped while test was waiting for its next notification")
547        }
548    }
549
550    pub fn condition(
551        &self,
552        cx: &TestAppContext,
553        mut predicate: impl FnMut(&T, &AppContext) -> bool,
554    ) -> impl Future<Output = ()> {
555        use postage::prelude::{Sink as _, Stream as _};
556
557        let (tx, mut rx) = postage::mpsc::channel(1024);
558        let timeout_duration = cx.condition_duration();
559
560        let mut cx = cx.cx.borrow_mut();
561        let subscriptions = (
562            cx.observe(self, {
563                let mut tx = tx.clone();
564                move |_, _| {
565                    tx.blocking_send(()).ok();
566                }
567            }),
568            cx.subscribe(self, {
569                let mut tx = tx.clone();
570                move |_, _, _| {
571                    tx.blocking_send(()).ok();
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}