use super::TestServices;
use crate::platform::{IPlatformIconLoader, IWindowingPlatform};
use crate::{Application, ApplicationImpl, Control};
use ferroui_base::animation::IGlobalClock;
use ferroui_base::input::platform::PlatformHotkeyConfiguration;
use ferroui_base::input::{
IAccessKeyHandler, IInputManager, IKeyboardDevice, IKeyboardNavigationHandler, IMouseDevice,
};
use ferroui_base::platform::{
DefaultPlatformSettings, IAssetLoader, ICursorFactory, IPlatformRenderInterface, IPlatformSettings,
};
use ferroui_base::reactive::IDisposable;
use ferroui_base::styling::{IGlobalStyles, Style};
use ferroui_base::threading::{Dispatcher, FerroSynchronizationContext, UnitTestDispatcherScope};
use ferroui_base::{ferro_class, instantiate, FerroLocator, FerroObjectImpl, FerroObjectImplExt, Ref};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
#[repr(C)]
pub struct UnitTestApplication {
base: Application,
services: TestServices,
}
ferro_class!(UnitTestApplication: Application);
impl FerroObjectImpl for UnitTestApplication {
fn constructed(this: &Self) {
Self::parent_constructed(this);
Application::bind_current(this.to_ref().upcast());
this.register_services();
}
}
impl ApplicationImpl for UnitTestApplication {
fn register_services(this: &Self) {
this.services().register(this);
}
}
impl TestServices {
pub fn register(&self, this: &Application) {
let services = self;
let locator = FerroLocator::current_mutable();
bind(&locator, services.asset_loader.clone() as Option<Rc<dyn IAssetLoader>>);
bind(&locator, services.global_clock.clone() as Option<Rc<dyn IGlobalClock>>);
locator.bind::<dyn IGlobalStyles>().to_constant(this.as_global_styles());
bind(&locator, services.input_manager.clone() as Option<Rc<dyn IInputManager>>);
bind(
&locator,
services
.input_manager
.as_ref()
.map(|input_manager| crate::ToolTipService::new(input_manager) as Rc<dyn crate::IToolTipService>),
);
bind(&locator, services.keyboard_device.as_ref().and_then(|f| f()) as Option<Rc<dyn IKeyboardDevice>>);
bind(&locator, services.mouse_device.as_ref().and_then(|f| f()) as Option<Rc<dyn IMouseDevice>>);
bind_func::<dyn IKeyboardNavigationHandler>(&locator, services.keyboard_navigation.clone());
bind(&locator, services.render_interface.clone() as Option<Rc<dyn IPlatformRenderInterface>>);
if let Some(font_manager_impl) = services.font_manager_impl.clone() {
locator.bind::<dyn ferroui_base::platform::IFontManagerImpl>().to_constant(font_manager_impl);
}
if let Some(text_shaper_impl) = services.text_shaper_impl.clone() {
locator.bind::<dyn ferroui_base::platform::ITextShaperImpl>().to_constant(text_shaper_impl);
}
bind(&locator, services.standard_cursor_factory.clone() as Option<Rc<dyn ICursorFactory>>);
bind(&locator, services.windowing_platform.clone() as Option<Rc<dyn IWindowingPlatform>>);
if let Some(platform) = services.platform.clone() {
locator.bind::<dyn ferroui_base::platform::IRuntimePlatform>().to_constant(platform);
}
if let Some(icon_loader) = services.icon_loader.clone() {
locator.bind::<dyn IPlatformIconLoader>().to_constant(icon_loader);
}
locator.bind_to_self_singleton::<PlatformHotkeyConfiguration>();
let platform_settings: Rc<dyn IPlatformSettings> =
services.platform_settings.clone().unwrap_or_else(|| Rc::new(DefaultPlatformSettings::new()));
locator.bind::<dyn IPlatformSettings>().to_constant(platform_settings);
bind_func::<dyn IAccessKeyHandler>(&locator, services.access_key_handler.clone());
super::NullRenderer::register();
this.initialize_theme_variant();
let theme = services.theme.as_ref().map(|theme| theme());
if let Some(theme) = theme {
match theme.as_object().and_then(|object| object.downcast_ref::<Style>()) {
Some(styles) => this.styles().add_range(styles.children().snapshot().iter().cloned()),
None => this.styles().add(theme),
}
}
}
}
fn bind<T: ?Sized + 'static>(locator: &FerroLocator, service: Option<Rc<T>>) {
match service {
Some(service) => locator.bind::<T>().to_constant(service),
None => locator.bind::<T>().to_func(|| None),
};
}
fn bind_func<T: ?Sized + 'static>(locator: &FerroLocator, factory: Option<Rc<dyn Fn() -> Option<Rc<T>>>>) {
match factory {
Some(factory) => locator.bind::<T>().to_func(move || factory()),
None => locator.bind::<T>().to_func(|| None),
};
}
impl UnitTestApplication {
pub fn new(services: TestServices) -> Ref<Self> {
instantiate(Self::construct(services))
}
pub fn construct(services: TestServices) -> Self {
Self { base: Application::construct(), services }
}
pub fn current() -> Option<Ref<UnitTestApplication>> {
Application::current().and_then(|application| application.cast::<UnitTestApplication>())
}
pub fn services(&self) -> &TestServices {
&self.services
}
pub fn start(services: TestServices) -> UnitTestApplicationScope {
Self::start_scope(|| {
let _ = UnitTestApplication::new(services);
})
.1
}
pub fn start_with(services: TestServices, create: impl FnOnce() -> Ref<Application>) -> UnitTestApplicationScope {
let (application, scope) = Self::start_scope(|| {
let application = create();
Application::bind_current(application.clone());
services.register(&application);
application
});
application.initialize();
scope
}
fn start_scope<R>(create: impl FnOnce() -> R) -> (R, UnitTestApplicationScope) {
let dispatcher = Dispatcher::unit_test_scope();
Control::reset_loaded_queue_for_unit_tests();
let scope = FerroLocator::enter_scope();
let old_context = FerroSynchronizationContext::current();
let created = create();
Dispatcher::reset_before_unit_tests();
(created, UnitTestApplicationScope { state: RefCell::new(Some(ScopeState { scope, old_context, dispatcher })) })
}
}
struct ScopeState {
scope: Rc<dyn IDisposable>,
old_context: Option<Arc<FerroSynchronizationContext>>,
dispatcher: UnitTestDispatcherScope,
}
pub struct UnitTestApplicationScope {
state: RefCell<Option<ScopeState>>,
}
impl UnitTestApplicationScope {
pub fn dispose(&self) {
let Some(state) = self.state.borrow_mut().take() else { return };
if !std::thread::panicking() {
let dispatcher = Dispatcher::ui_thread();
if dispatcher.check_access() {
dispatcher.run_jobs(None);
}
let tool_tip_service = ferroui_base::LocatorExtensions::get_service::<dyn crate::IToolTipService>(
&*FerroLocator::current(),
);
if let Some(tool_tip_service) = tool_tip_service.as_ref().and_then(|service| service.as_tool_tip_service()) {
tool_tip_service.dispose();
}
if let Some(font_manager) = ferroui_base::LocatorExtensions::get_service::<ferroui_base::media::FontManager>(
&*FerroLocator::current(),
) {
font_manager.dispose();
}
Dispatcher::reset_for_unit_tests();
}
state.scope.dispose();
Dispatcher::reset_before_unit_tests();
FerroSynchronizationContext::set_current(state.old_context);
Control::reset_loaded_queue_for_unit_tests();
drop(state.dispatcher);
}
}
impl IDisposable for UnitTestApplicationScope {
fn dispose(&self) {
UnitTestApplicationScope::dispose(self)
}
}
impl Drop for UnitTestApplicationScope {
fn drop(&mut self) {
UnitTestApplicationScope::dispose(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestIconLoader;
use ferroui_base::{ferro_impl_classes, LocatorExtensions};
use std::cell::Cell;
#[repr(C)]
struct SampleApplication {
base: Application,
initialized_with_the_services: Cell<bool>,
}
ferro_class!(SampleApplication: Application);
ferro_impl_classes!(SampleApplication: FerroObjectImpl);
impl ApplicationImpl for SampleApplication {
fn initialize(this: &Self) {
let is_current = Application::current().and_then(|application| application.cast::<SampleApplication>()).is_some();
let has_services = FerroLocator::current().get_service::<dyn IPlatformIconLoader>().is_some();
this.initialized_with_the_services.set(is_current && has_services);
}
}
#[test]
fn an_application_class_starts_as_the_current_application_with_the_test_services() {
let services = TestServices::new().with_icon_loader(Rc::new(TestIconLoader));
let _app = UnitTestApplication::start_with(services, || {
instantiate(SampleApplication { base: Application::construct(), initialized_with_the_services: Cell::new(false) })
.upcast()
});
let application = Application::current().and_then(|application| application.cast::<SampleApplication>());
assert!(application.expect("the application of the test").initialized_with_the_services.get());
assert!(UnitTestApplication::current().is_none());
assert!(FerroLocator::current().get_service::<dyn IGlobalStyles>().is_some());
}
}