#![cfg(all(test, full_widgets))]
use crate::core::Rect;
use crate::widget::capability::types::CapabilityValue;
use crate::widget::capability::{
widget_property_get, widget_property_names, widget_property_set, WidgetFactory,
BASE_PROPERTY_NAMES,
};
use crate::widget::{CapabilityAccessError, Widget};
fn assert_contract(widget: &mut dyn Widget, label: &str) {
let names = widget_property_names(widget).unwrap_or_else(|| {
panic!("{label}: must impl WidgetProperties and expose it via dyn Widget")
});
for name in names {
assert!(
widget_property_get(widget, name).is_ok(),
"{label}: property_names() publishes {name:?} but get() rejects it"
);
}
for shared in BASE_PROPERTY_NAMES {
assert!(
widget_property_get(widget, shared).is_ok(),
"{label}: shared property {shared:?} must be reachable through the base fallback"
);
assert!(
names.contains(shared),
"{label}: shared property {shared:?} must also be published so schema consumers see it"
);
}
assert_eq!(
widget_property_get(widget, "definitely_not_a_property"),
Err(CapabilityAccessError::UnknownProperty),
"{label}: an unknown name must be reported as UnknownProperty"
);
assert_eq!(
widget_property_set(widget, "definitely_not_a_property", CapabilityValue::Null),
Err(CapabilityAccessError::UnknownProperty),
"{label}: the mutable hook must reach the concrete implementation"
);
}
fn get(widget: &dyn Widget, name: &str) -> CapabilityValue {
widget_property_get(widget, name).unwrap_or_else(|err| panic!("{name:?} should read: {err:?}"))
}
#[test]
fn base_widgets_satisfy_the_contract() {
use crate::widget::base_widgets::button::Button;
use crate::widget::base_widgets::checkbox::CheckBox;
use crate::widget::base_widgets::label::Label;
use crate::widget::base_widgets::radiobutton::RadioButton;
use crate::widget::base_widgets::toggle_button::ToggleButton;
let mut button = Button::new("ok".to_string(), Rect::new(0, 0, 80, 24));
assert_eq!(
widget_property_set(&mut button, "text", CapabilityValue::String("go".into())),
Ok(())
);
assert_eq!(get(&button, "text"), CapabilityValue::String("go".into()));
assert_contract(&mut button, "Button");
let mut checkbox = CheckBox::new(Rect::new(0, 0, 120, 24));
assert_eq!(widget_property_set(&mut checkbox, "checked", CapabilityValue::Bool(true)), Ok(()));
assert_eq!(get(&checkbox, "checked"), CapabilityValue::Bool(true));
assert_contract(&mut checkbox, "CheckBox");
let mut radio = RadioButton::new(Rect::new(0, 0, 120, 24));
assert_contract(&mut radio, "RadioButton");
let mut label = Label::new("hello".to_string(), Rect::new(0, 0, 80, 20));
assert_eq!(get(&label, "text"), CapabilityValue::String("hello".into()));
assert_contract(&mut label, "Label");
let mut toggle = ToggleButton::new("t".to_string(), Rect::new(0, 0, 80, 24));
assert_contract(&mut toggle, "ToggleButton");
}
#[test]
fn input_widgets_satisfy_the_contract() {
use crate::widget::display_widgets::progressbar::ProgressBar;
use crate::widget::input_widgets::lineedit::LineEdit;
use crate::widget::input_widgets::listbox::ListBox;
use crate::widget::input_widgets::spinbox::SpinBox;
let mut slider = crate::widget::Slider::new(Rect::new(0, 0, 100, 20));
assert_eq!(widget_property_set(&mut slider, "value", CapabilityValue::Int(30)), Ok(()));
assert_eq!(get(&slider, "value"), CapabilityValue::Int(30));
assert_contract(&mut slider, "Slider");
let mut spinbox = SpinBox::new(Rect::new(0, 0, 60, 24));
assert_eq!(widget_property_set(&mut spinbox, "value", CapabilityValue::Int(7)), Ok(()));
assert_eq!(get(&spinbox, "value"), CapabilityValue::Int(7));
assert_contract(&mut spinbox, "SpinBox");
let mut edit = LineEdit::new(Rect::new(0, 0, 80, 24));
assert_eq!(
widget_property_set(&mut edit, "text", CapabilityValue::String("hi".into())),
Ok(())
);
assert_eq!(get(&edit, "text"), CapabilityValue::String("hi".into()));
assert_contract(&mut edit, "LineEdit");
let mut list = ListBox::new(Rect::new(0, 0, 120, 100));
assert_eq!(get(&list, "selection_mode"), CapabilityValue::String("single".into()));
assert_eq!(
widget_property_set(&mut list, "selection_mode", CapabilityValue::String("multi".into())),
Ok(())
);
assert_eq!(get(&list, "selection_mode"), CapabilityValue::String("multi".into()));
assert_contract(&mut list, "ListBox");
let mut progress = ProgressBar::new(Rect::new(0, 0, 100, 20));
assert_eq!(
widget_property_set(&mut progress, "progress", CapabilityValue::Float(0.5)),
Err(CapabilityAccessError::ReadOnlyProperty)
);
assert_contract(&mut progress, "ProgressBar");
}
#[test]
fn container_widgets_satisfy_the_contract() {
use crate::widget::container_widgets::collapsible_pane::CollapsiblePane;
use crate::widget::container_widgets::groupbox::GroupBox;
use crate::widget::container_widgets::scrollarea::ScrollArea;
use crate::widget::container_widgets::splitter::Splitter;
use crate::widget::container_widgets::tabwidget::TabWidget;
let mut group = GroupBox::new(Rect::new(0, 0, 200, 150));
assert_contract(&mut group, "GroupBox");
let mut splitter = Splitter::new(Rect::new(0, 0, 300, 200));
assert_eq!(get(&splitter, "pane_count"), CapabilityValue::UInt(0));
assert_contract(&mut splitter, "Splitter");
let mut area = ScrollArea::new(Rect::new(0, 0, 300, 200));
assert!(widget_property_get(&area, "horizontal_scroll_bar_policy").is_ok());
assert_contract(&mut area, "ScrollArea");
let mut tabs = TabWidget::new(Rect::new(0, 0, 300, 200));
assert_eq!(get(&tabs, "tab_count"), CapabilityValue::UInt(0));
assert_contract(&mut tabs, "TabWidget");
let mut pane = CollapsiblePane::new(Rect::new(0, 0, 200, 100), "T".to_string());
assert_eq!(widget_property_set(&mut pane, "collapsed", CapabilityValue::Bool(true)), Ok(()));
assert_eq!(get(&pane, "collapsed"), CapabilityValue::Bool(true));
assert_contract(&mut pane, "CollapsiblePane");
}
#[test]
fn dialog_widgets_satisfy_the_contract() {
use crate::widget::dialog::file_dialog::FileDialog;
use crate::widget::dialog::message_box::MessageBox;
use crate::widget::dialog::popup_window::PopupWindow;
use crate::widget::dialog::progress_dialog::ProgressDialog;
let mut message = MessageBox::new(Rect::new(0, 0, 350, 150));
assert_eq!(
widget_property_set(&mut message, "title", CapabilityValue::String("t".into())),
Ok(())
);
assert_eq!(get(&message, "title"), CapabilityValue::String("t".into()));
assert_eq!(
widget_property_set(&mut message, "text", CapabilityValue::String("body".into())),
Ok(())
);
assert_eq!(get(&message, "text"), CapabilityValue::String("body".into()));
assert_contract(&mut message, "MessageBox");
let mut file = FileDialog::new(Rect::new(0, 0, 500, 400));
assert!(widget_property_get(&file, "modal").is_ok());
assert_contract(&mut file, "FileDialog");
let mut progress = ProgressDialog::new(Rect::new(0, 0, 350, 120));
assert_contract(&mut progress, "ProgressDialog");
let mut popup = PopupWindow::new(Rect::new(0, 0, 200, 150));
assert_eq!(get(&popup, "has_content"), CapabilityValue::Bool(false));
assert_contract(&mut popup, "PopupWindow");
}
#[test]
fn color_picker_satisfies_the_contract() {
use crate::widget::special_widgets::color_picker::ColorPicker;
let mut picker = ColorPicker::new(Rect::new(0, 0, 200, 150));
assert!(widget_property_get(&picker, "hex_rgba").is_ok());
assert_eq!(widget_property_set(&mut picker, "show_alpha", CapabilityValue::Bool(true)), Ok(()));
assert_eq!(get(&picker, "show_alpha"), CapabilityValue::Bool(true));
assert_eq!(
widget_property_set(&mut picker, "hex_rgba", CapabilityValue::String("nonsense".into())),
Err(CapabilityAccessError::TypeMismatch)
);
assert_contract(&mut picker, "ColorPicker");
}
#[test]
fn media_widgets_satisfy_the_contract() {
use crate::widget::media_widgets::animated_image::AnimatedImage;
use crate::widget::media_widgets::camera_preview::CameraPreview;
use crate::widget::media_widgets::hero_animation::HeroAnimation;
use crate::widget::media_widgets::lottie_widget::LottieWidget;
use crate::widget::media_widgets::rive_widget::RiveWidget;
use crate::widget::media_widgets::video_player::VideoPlayer;
let mut animated = AnimatedImage::new(Rect::new(0, 0, 100, 100));
assert_eq!(get(&animated, "playing"), CapabilityValue::Bool(false));
assert_eq!(widget_property_set(&mut animated, "playing", CapabilityValue::Bool(true)), Ok(()));
assert_contract(&mut animated, "AnimatedImage");
let mut hero = HeroAnimation::new(Rect::new(0, 0, 100, 100));
assert!(widget_property_get(&hero, "animation_progress").is_ok());
assert_contract(&mut hero, "HeroAnimation");
let mut lottie = LottieWidget::new(Rect::new(0, 0, 100, 100));
assert_contract(&mut lottie, "LottieWidget");
let mut rive = RiveWidget::new(Rect::new(0, 0, 100, 100));
assert_contract(&mut rive, "RiveWidget");
let mut video = VideoPlayer::new(Rect::new(0, 0, 320, 240));
assert!(widget_property_get(&video, "volume").is_ok());
assert_contract(&mut video, "VideoPlayer");
let mut camera = CameraPreview::new(Rect::new(0, 0, 320, 240));
assert_eq!(get(&camera, "is_active"), CapabilityValue::Bool(false));
assert_contract(&mut camera, "CameraPreview");
}
#[test]
fn advanced_and_menu_widgets_satisfy_the_contract() {
use crate::widget::advanced_widgets::calendar::Calendar;
use crate::widget::advanced_widgets::tab_bar::TabBar;
use crate::widget::menu_toolbar::menu_bar::MenuBar;
use crate::widget::menu_toolbar::status_bar::StatusBar;
let mut calendar = Calendar::new(Rect::new(0, 0, 300, 220));
assert_contract(&mut calendar, "Calendar");
let mut tab_bar = TabBar::new(Rect::new(0, 0, 300, 28));
assert_contract(&mut tab_bar, "TabBar");
let mut menu_bar = MenuBar::new(Rect::new(0, 0, 400, 24));
assert_contract(&mut menu_bar, "MenuBar");
let mut status_bar = StatusBar::new(Rect::new(0, 0, 400, 22));
assert_contract(&mut status_bar, "StatusBar");
}
#[test]
fn view_widgets_satisfy_the_contract() {
use crate::widget::view_widgets::data_grid::DataGrid;
use crate::widget::view_widgets::list_view::ListView;
use crate::widget::view_widgets::tree_view::TreeView;
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
assert_contract(&mut grid, "DataGrid");
let mut list = ListView::new(Rect::new(0, 0, 300, 200));
assert_contract(&mut list, "ListView");
let mut tree = TreeView::new(Rect::new(0, 0, 300, 200));
assert_contract(&mut tree, "TreeView");
}
#[test]
fn cupertino_widgets_satisfy_the_contract() {
use crate::widget::cupertino::CupertinoAlertDialog;
use crate::widget::cupertino::CupertinoDatePicker;
use crate::widget::cupertino::CupertinoNavigationBar;
use crate::widget::cupertino::CupertinoSegmentedControl;
use crate::widget::special_widgets::snackbar::Snackbar;
let mut dialog = CupertinoAlertDialog::new(Rect::new(0, 0, 270, 150));
assert_eq!(get(&dialog, "title"), CapabilityValue::String(String::new()));
assert_eq!(
widget_property_set(&mut dialog, "title", CapabilityValue::String("T".into())),
Ok(())
);
assert_eq!(get(&dialog, "title"), CapabilityValue::String("T".into()));
assert_eq!(
widget_property_set(&mut dialog, "message", CapabilityValue::String("m".into())),
Ok(())
);
assert_eq!(get(&dialog, "message"), CapabilityValue::String("m".into()));
assert_contract(&mut dialog, "CupertinoAlertDialog");
let mut bar = CupertinoNavigationBar::new(Rect::new(0, 0, 375, 96));
assert_eq!(get(&bar, "large_title"), CapabilityValue::Bool(true));
assert_eq!(widget_property_set(&mut bar, "large_title", CapabilityValue::Bool(false)), Ok(()));
assert_eq!(get(&bar, "large_title"), CapabilityValue::Bool(false));
assert_contract(&mut bar, "CupertinoNavigationBar");
let mut segmented = CupertinoSegmentedControl::new(Rect::new(0, 0, 300, 32));
assert_eq!(get(&segmented, "segment_count"), CapabilityValue::UInt(0));
segmented.set_segments(vec!["A".to_string(), "B".to_string(), "C".to_string()]);
assert_eq!(get(&segmented, "segment_count"), CapabilityValue::UInt(3));
assert_eq!(
widget_property_set(&mut segmented, "selected_index", CapabilityValue::UInt(2)),
Ok(())
);
assert_eq!(get(&segmented, "selected_index"), CapabilityValue::UInt(2));
assert_eq!(
widget_property_set(&mut segmented, "segment_count", CapabilityValue::UInt(1)),
Err(CapabilityAccessError::ReadOnlyProperty)
);
assert_contract(&mut segmented, "CupertinoSegmentedControl");
let mut picker = CupertinoDatePicker::new(Rect::new(0, 0, 300, 200));
assert_eq!(get(&picker, "selected_date"), CapabilityValue::String("2025-01-01".into()));
assert_eq!(
widget_property_set(
&mut picker,
"selected_date",
CapabilityValue::String("2024-02-29".into())
),
Ok(())
);
assert_eq!(get(&picker, "selected_date"), CapabilityValue::String("2024-02-29".into()));
assert_eq!(
widget_property_set(
&mut picker,
"selected_date",
CapabilityValue::String("not-a-date".into())
),
Err(CapabilityAccessError::TypeMismatch)
);
assert_contract(&mut picker, "CupertinoDatePicker");
let mut snackbar = Snackbar::new(Rect::new(0, 0, 420, 120));
assert_eq!(
widget_property_set(&mut snackbar, "message", CapabilityValue::String("hi".into())),
Ok(())
);
assert_eq!(get(&snackbar, "message"), CapabilityValue::String("hi".into()));
assert_contract(&mut snackbar, "Snackbar");
}
#[test]
fn menu_and_property_controls_satisfy_the_contract() {
use crate::widget::menu_toolbar::dropdown_menu::DropdownMenu;
use crate::widget::menu_toolbar::menu_button::MenuButton;
use crate::widget::view_widgets::properties_panel::PropertiesPanel;
let mut dropdown = DropdownMenu::new(Rect::new(0, 0, 200, 32));
assert_eq!(get(&dropdown, "selected_index"), CapabilityValue::Null);
dropdown.add_item(crate::widget::menu_toolbar::dropdown_menu::DropdownItem::new("a", "A"));
dropdown.add_item(crate::widget::menu_toolbar::dropdown_menu::DropdownItem::new("b", "B"));
assert_eq!(get(&dropdown, "item_count"), CapabilityValue::UInt(2));
assert_eq!(
widget_property_set(&mut dropdown, "selected_index", CapabilityValue::UInt(1)),
Ok(())
);
assert_eq!(get(&dropdown, "selected_index"), CapabilityValue::UInt(1));
assert_eq!(widget_property_set(&mut dropdown, "expanded", CapabilityValue::Bool(true)), Ok(()));
assert_eq!(get(&dropdown, "expanded"), CapabilityValue::Bool(true));
assert_eq!(
widget_property_set(&mut dropdown, "item_count", CapabilityValue::UInt(9)),
Err(CapabilityAccessError::ReadOnlyProperty)
);
assert_contract(&mut dropdown, "DropdownMenu");
let mut menu = MenuButton::new("File", Rect::new(0, 0, 120, 28));
assert_eq!(get(&menu, "text"), CapabilityValue::String("File".into()));
assert_eq!(
widget_property_set(&mut menu, "text", CapabilityValue::String("Edit".into())),
Ok(())
);
assert_eq!(get(&menu, "text"), CapabilityValue::String("Edit".into()));
assert_eq!(widget_property_set(&mut menu, "expanded", CapabilityValue::Bool(true)), Ok(()));
assert_eq!(get(&menu, "expanded"), CapabilityValue::Bool(true));
assert_eq!(
widget_property_set(&mut menu, "item_count", CapabilityValue::UInt(9)),
Err(CapabilityAccessError::ReadOnlyProperty)
);
assert_contract(&mut menu, "MenuButton");
let mut panel = PropertiesPanel::new(Rect::new(0, 0, 300, 400));
assert_eq!(get(&panel, "property_count"), CapabilityValue::UInt(0));
panel.add_property(crate::widget::view_widgets::properties_panel::PropertyEntry::new(
"width",
crate::widget::view_widgets::properties_panel::PropertyValue::Number(10.0),
None,
None,
true,
));
assert_eq!(get(&panel, "property_count"), CapabilityValue::UInt(1));
assert_eq!(
widget_property_set(&mut panel, "property_count", CapabilityValue::UInt(9)),
Err(CapabilityAccessError::ReadOnlyProperty)
);
assert_contract(&mut panel, "PropertiesPanel");
}
#[test]
fn controls_without_own_properties_still_publish_the_shared_set() {
use crate::widget::input_widgets::textedit::TextEdit;
let mut edit = TextEdit::new(Rect::new(0, 0, 200, 24));
let names = widget_property_names(&edit).expect("TextEdit must expose its contract");
for shared in BASE_PROPERTY_NAMES {
assert!(
names.contains(shared),
"TextEdit declares no own properties, but the shared {shared:?} must still be published"
);
}
assert_contract(&mut edit, "TextEdit");
}
#[test]
fn a_widget_without_a_contract_reports_none_not_empty() {
use crate::event::{Event, EventHandler};
use crate::widget::base::BaseWidget;
use crate::widget::{Widget, WidgetKind};
struct Unmigrated {
base: BaseWidget,
}
impl Widget for Unmigrated {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
}
impl EventHandler for Unmigrated {
fn handle_event(&mut self, _event: &Event) {}
}
let mut widget = Unmigrated {
base: BaseWidget::new(WidgetKind::Panel, Rect::new(0, 0, 10, 10), "unmigrated"),
};
let dyn_widget: &mut dyn Widget = &mut widget;
assert!(
widget_property_names(dyn_widget).is_none(),
"a widget with no WidgetProperties impl must report None, not an empty list"
);
assert_eq!(
widget_property_get(dyn_widget, "text"),
Err(CapabilityAccessError::UnsupportedOnWidget),
"reflection must say the control has no contract, not that the name is wrong"
);
}
#[test]
fn every_factory_widget_declares_a_property_contract() {
let factory = WidgetFactory::new_with_defaults();
let names = factory.widget_names();
assert!(!names.is_empty(), "the factory must register widgets");
let mut contractless = Vec::new();
for name in names {
let Some(widget) = factory.create(name, Rect::new(0, 0, 64, 48), "x") else {
continue;
};
if widget.properties_dyn().is_none() {
contractless.push(name);
}
}
assert!(
contractless.is_empty(),
"these widgets are constructible but expose no WidgetProperties contract, so \
every property read/write against them reports `UnsupportedOnWidget`: \
{contractless:?}"
);
}
#[test]
fn no_published_property_answers_unknown_when_written() {
let factory = WidgetFactory::new_with_defaults();
let mut offenders = alloc::vec::Vec::new();
for name in factory.widget_names() {
let Some(mut widget) = factory.create(name, Rect::new(0, 0, 64, 48), "x") else {
continue;
};
let Some(published) = widget_property_names(widget.as_ref()) else {
continue;
};
for property in published {
if widget_property_get(widget.as_ref(), property).is_err() {
continue;
}
if widget_property_set(widget.as_mut(), property, CapabilityValue::Bool(false))
== Err(CapabilityAccessError::UnknownProperty)
{
offenders.push((name, property));
}
}
}
assert!(
offenders.is_empty(),
"these properties are published by property_names() but answer UnknownProperty when \
written, which contradicts the contract — a name with no setter must answer \
ReadOnlyProperty (widget, property): {offenders:?}"
);
}
#[test]
fn schema_and_contract_publish_the_same_names() {
let factory = WidgetFactory::new_with_defaults();
let mut missing = alloc::vec::Vec::new();
for capability in factory.capabilities() {
let Some(widget) = factory.create(capability.canonical_name, Rect::new(0, 0, 64, 48), "x")
else {
continue;
};
let Some(published) = widget_property_names(widget.as_ref()) else {
continue;
};
let declared: alloc::vec::Vec<&str> =
capability.properties.iter().map(|schema| schema.name).collect();
for name in published {
if !declared.contains(name) {
missing.push((capability.canonical_name, name));
}
}
}
assert!(
missing.is_empty(),
"these controls publish properties their registered schema does not declare, so the \
schema and the contract disagree about what exists (widget, property): {missing:?}"
);
}
#[test]
fn every_shared_kind_has_a_tie_break() {
let factory = WidgetFactory::new_with_defaults();
let mut by_kind: alloc::collections::BTreeMap<
crate::widget::WidgetKind,
alloc::vec::Vec<&'static str>,
> = alloc::collections::BTreeMap::new();
for capability in factory.capabilities() {
by_kind.entry(capability.kind).or_default().push(capability.canonical_name);
}
let mut ambiguous = alloc::vec::Vec::new();
for (kind, mut names) in by_kind {
if names.len() < 2 {
continue;
}
names.sort_unstable();
for name in &names {
let Some(widget) = factory.create(name, Rect::new(0, 0, 32, 32), "") else {
continue;
};
let resolved = factory.capability_for_kind_instance(widget.as_ref());
let resolved_name = resolved.map(|cap| cap.canonical_name);
if resolved_name != Some(*name) {
ambiguous.push((kind, *name, resolved_name));
}
}
}
assert!(
ambiguous.is_empty(),
"these controls share a WidgetKind but the factory cannot resolve them back \
to their own capability, so they would read another control's schema \
(kind, created-as, resolved-as): {ambiguous:?}"
);
}