use crate::{ AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; #[derive(Clone)] pub struct TestAppContext { pub app: Rc, pub background_executor: BackgroundExecutor, pub foreground_executor: ForegroundExecutor, pub dispatcher: TestDispatcher, } impl Context for TestAppContext { type Result = T; fn build_model( &mut self, build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Self::Result> where T: 'static, { let mut app = self.app.borrow_mut(); app.build_model(build_model) } fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> Self::Result { let mut app = self.app.borrow_mut(); app.update_model(handle, update) } fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, { let mut lock = self.app.borrow_mut(); lock.update_window(window, f) } fn read_model( &self, handle: &Model, read: impl FnOnce(&T, &AppContext) -> R, ) -> Self::Result where T: 'static, { let app = self.app.borrow(); app.read_model(handle, read) } fn read_window( &self, window: &WindowHandle, read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, { let app = self.app.borrow(); app.read_window(window, read) } } impl TestAppContext { pub fn new(dispatcher: TestDispatcher) -> Self { let arc_dispatcher = Arc::new(dispatcher.clone()); let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(arc_dispatcher); let platform = Rc::new(TestPlatform::new( background_executor.clone(), foreground_executor.clone(), )); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); Self { app: AppContext::new(platform, asset_source, http_client), background_executor, foreground_executor, dispatcher: dispatcher.clone(), } } pub fn new_app(&self) -> TestAppContext { Self::new(self.dispatcher.clone()) } pub fn quit(&self) { self.app.borrow_mut().quit(); } pub fn refresh(&mut self) -> Result<()> { let mut app = self.app.borrow_mut(); app.refresh(); Ok(()) } pub fn executor(&self) -> BackgroundExecutor { self.background_executor.clone() } pub fn foreground_executor(&self) -> &ForegroundExecutor { &self.foreground_executor } pub fn update(&self, f: impl FnOnce(&mut AppContext) -> R) -> R { let mut cx = self.app.borrow_mut(); cx.update(f) } pub fn read(&self, f: impl FnOnce(&AppContext) -> R) -> R { let cx = self.app.borrow(); f(&*cx) } pub fn add_window(&mut self, build_window: F) -> WindowHandle where F: FnOnce(&mut ViewContext) -> V, V: Render, { let mut cx = self.app.borrow_mut(); cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) } pub fn add_window_view(&mut self, build_window: F) -> (View, VisualTestContext) where F: FnOnce(&mut ViewContext) -> V, V: Render, { let mut cx = self.app.borrow_mut(); let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); drop(cx); let view = window.root_view(self).unwrap(); (view, VisualTestContext::from_window(*window.deref(), self)) } pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, R: 'static, { self.foreground_executor.spawn(f(self.to_async())) } pub fn has_global(&self) -> bool { let app = self.app.borrow(); app.has_global::() } pub fn read_global(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R { let app = self.app.borrow(); read(app.global(), &app) } pub fn try_read_global( &self, read: impl FnOnce(&G, &AppContext) -> R, ) -> Option { let lock = self.app.borrow(); Some(read(lock.try_global()?, &lock)) } pub fn set_global(&mut self, global: G) { let mut lock = self.app.borrow_mut(); lock.set_global(global); } pub fn update_global( &mut self, update: impl FnOnce(&mut G, &mut AppContext) -> R, ) -> R { let mut lock = self.app.borrow_mut(); lock.update_global(update) } pub fn to_async(&self) -> AsyncAppContext { AsyncAppContext { app: Rc::downgrade(&self.app), background_executor: self.background_executor.clone(), foreground_executor: self.foreground_executor.clone(), } } pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, keystroke: Keystroke, is_held: bool, ) { let handled = window .update(self, |_, cx| { cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held })) }) .is_ok_and(|handled| handled); if !handled { // todo!() simluate input here } } pub fn notifications(&mut self, entity: &Model) -> impl Stream { let (tx, rx) = futures::channel::mpsc::unbounded(); entity.update(self, move |_, cx: &mut ModelContext| { cx.observe(entity, { let tx = tx.clone(); move |_, _, _| { let _ = tx.unbounded_send(()); } }) .detach(); cx.on_release(move |_, _| tx.close_channel()).detach(); }); rx } pub fn events>( &mut self, entity: &Model, ) -> futures::channel::mpsc::UnboundedReceiver where Evt: 'static + Clone, { let (tx, rx) = futures::channel::mpsc::unbounded(); entity .update(self, |_, cx: &mut ModelContext| { cx.subscribe(entity, move |_model, _handle, event, _cx| { let _ = tx.unbounded_send(event.clone()); }) }) .detach(); rx } pub async fn condition( &mut self, model: &Model, mut predicate: impl FnMut(&mut T, &mut ModelContext) -> bool, ) { let timer = self.executor().timer(Duration::from_secs(3)); let mut notifications = self.notifications(model); use futures::FutureExt as _; use smol::future::FutureExt as _; async { while notifications.next().await.is_some() { if model.update(self, &mut predicate) { return Ok(()); } } bail!("model dropped") } .race(timer.map(|_| Err(anyhow!("condition timed out")))) .await .unwrap(); } } impl Model { pub fn next_event(&self, cx: &mut TestAppContext) -> Evt where Evt: Send + Clone + 'static, T: EventEmitter, { let (tx, mut rx) = futures::channel::mpsc::unbounded(); let _subscription = self.update(cx, |_, cx| { cx.subscribe(self, move |_, _, event, _| { tx.unbounded_send(event.clone()).ok(); }) }); cx.executor().run_until_parked(); rx.try_next() .expect("no event received") .expect("model was dropped") } } impl View { pub fn condition( &self, cx: &TestAppContext, mut predicate: impl FnMut(&V, &AppContext) -> bool, ) -> impl Future where Evt: 'static, V: EventEmitter, { use postage::prelude::{Sink as _, Stream as _}; let (tx, mut rx) = postage::mpsc::channel(1024); let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration(); let mut cx = cx.app.borrow_mut(); let subscriptions = ( cx.observe(self, { let mut tx = tx.clone(); move |_, _| { tx.blocking_send(()).ok(); } }), cx.subscribe(self, { let mut tx = tx.clone(); move |_, _: &Evt, _| { tx.blocking_send(()).ok(); } }), ); let cx = cx.this.upgrade().unwrap(); let handle = self.downgrade(); async move { crate::util::timeout(timeout_duration, async move { loop { { let cx = cx.borrow(); let cx = &*cx; if predicate( handle .upgrade() .expect("view dropped with pending condition") .read(cx), cx, ) { break; } } // todo!(start_waiting) // cx.borrow().foreground_executor().start_waiting(); rx.recv() .await .expect("view dropped with pending condition"); // cx.borrow().foreground_executor().finish_waiting(); } }) .await .expect("condition timed out"); drop(subscriptions); } } } use derive_more::{Deref, DerefMut}; #[derive(Deref, DerefMut)] pub struct VisualTestContext<'a> { #[deref] #[deref_mut] cx: &'a mut TestAppContext, window: AnyWindowHandle, } impl<'a> VisualTestContext<'a> { pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } } impl<'a> Context for VisualTestContext<'a> { type Result = ::Result; fn build_model( &mut self, build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T, ) -> Self::Result> { self.cx.build_model(build_model) } fn update_model( &mut self, handle: &Model, update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R, ) -> Self::Result where T: 'static, { self.cx.update_model(handle, update) } fn read_model( &self, handle: &Model, read: impl FnOnce(&T, &AppContext) -> R, ) -> Self::Result where T: 'static, { self.cx.read_model(handle, read) } fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result where F: FnOnce(AnyView, &mut WindowContext<'_>) -> T, { self.cx.update_window(window, f) } fn read_window( &self, window: &WindowHandle, read: impl FnOnce(View, &AppContext) -> R, ) -> Result where T: 'static, { self.cx.read_window(window, read) } } impl<'a> VisualContext for VisualTestContext<'a> { fn build_view( &mut self, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where V: 'static + Render, { self.window .update(self.cx, |_, cx| cx.build_view(build_view)) .unwrap() } fn update_view( &mut self, view: &View, update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> Self::Result { self.window .update(self.cx, |_, cx| cx.update_view(view, update)) .unwrap() } fn replace_root_view( &mut self, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where V: Render, { self.window .update(self.cx, |_, cx| cx.replace_root_view(build_view)) .unwrap() } }