use compose_rust::prelude::*;
use compose_rust::protocol::{HostEvent, Mutation, PropertyValue, decode_batch, encode_event};
use compose_rust::{EventPayload, Host, PropertyKind, WidgetKind};
use std::cell::RefCell;
fn node_of(batch: &[u8], widget: WidgetKind) -> u32 {
decode_batch(batch)
.unwrap()
.into_iter()
.find_map(|mutation| match mutation {
Mutation::Create {
node_id,
widget: kind,
} if kind == widget => Some(node_id),
_ => None,
})
.unwrap_or_else(|| panic!("no {widget:?} was created"))
}
fn props_of(batch: &[u8], node: u32) -> Vec<(PropertyKind, PropertyValue<'static>)> {
decode_batch(batch)
.unwrap()
.into_iter()
.filter_map(|mutation| match mutation {
Mutation::SetProp {
node_id,
property,
value,
} if node_id == node => Some((
property,
match value {
PropertyValue::String(_) => PropertyValue::None,
PropertyValue::Bytes(_) => PropertyValue::None,
PropertyValue::None => PropertyValue::None,
PropertyValue::Bool(value) => PropertyValue::Bool(value),
PropertyValue::Integer(value) => PropertyValue::Integer(value),
PropertyValue::Float(value) => PropertyValue::Float(value),
},
)),
_ => None,
})
.collect()
}
fn handler_of(batch: &[u8], node: u32, property: PropertyKind) -> u64 {
props_of(batch, node)
.into_iter()
.find_map(|(kind, value)| match value {
PropertyValue::Integer(handler) if kind == property => Some(handler as u64),
_ => None,
})
.unwrap_or_else(|| panic!("node {node} has no {property:?}"))
}
#[test]
fn fr15_picker_widgets_keep_their_assigned_tags() {
assert_eq!(
[
WidgetKind::DatePicker as u16,
WidgetKind::TimePicker as u16,
WidgetKind::Dropdown as u16,
],
[27, 28, 29],
);
}
thread_local! {
static PICKED: RefCell<Vec<i64>> = const { RefCell::new(Vec::new()) };
}
fn date_app() -> Element {
rsx! {
DatePicker {
value: 20_352,
min: 20_000,
max: 20_500,
on_change: move |days: i64| PICKED.with(|picked| picked.borrow_mut().push(days)),
}
}
}
#[test]
fn fr15_date_picker_sends_a_value_and_a_range_as_epoch_days() {
let mut host = Host::new(date_app);
let batch = host.rebuild().unwrap().to_vec();
let node = node_of(&batch, WidgetKind::DatePicker);
let props = props_of(&batch, node);
assert!(props.contains(&(PropertyKind::Value, PropertyValue::Integer(20_352))));
assert!(props.contains(&(PropertyKind::Min, PropertyValue::Integer(20_000))));
assert!(props.contains(&(PropertyKind::Max, PropertyValue::Integer(20_500))));
}
#[test]
fn fr15_date_picker_reports_the_picked_day() {
PICKED.with(|picked| picked.borrow_mut().clear());
let mut host = Host::new(date_app);
let batch = host.rebuild().unwrap().to_vec();
let node = node_of(&batch, WidgetKind::DatePicker);
let handler = handler_of(&batch, node, PropertyKind::OnValueChange);
let mut encoded = Vec::new();
encode_event(
&HostEvent {
node_id: node,
handler_id: handler,
payload: EventPayload::ValueChanged(20_400.0),
},
&mut encoded,
)
.unwrap();
host.dispatch_event(&encoded).unwrap();
PICKED.with(|picked| assert_eq!(*picked.borrow(), vec![20_400]));
}
fn time_app() -> Element {
rsx! {
TimePicker { value: 13 * 60 + 45 }
}
}
#[test]
fn fr15_time_picker_sends_minutes_since_midnight() {
let mut host = Host::new(time_app);
let batch = host.rebuild().unwrap().to_vec();
let node = node_of(&batch, WidgetKind::TimePicker);
let props = props_of(&batch, node);
assert!(props.contains(&(PropertyKind::Value, PropertyValue::Integer(825))));
assert!(props.contains(&(PropertyKind::Min, PropertyValue::Integer(0))));
assert!(props.contains(&(PropertyKind::Max, PropertyValue::Integer(1_439))));
}
fn dropdown_app() -> Element {
rsx! {
Dropdown {
selected_index: 1,
on_change: move |index: usize| {
PICKED.with(|picked| picked.borrow_mut().push(index as i64))
},
Text { text: "first" }
Text { text: "second" }
}
}
}
#[test]
fn fr15_dropdown_carries_its_options_as_children() {
PICKED.with(|picked| picked.borrow_mut().clear());
let mut host = Host::new(dropdown_app);
let batch = host.rebuild().unwrap().to_vec();
let node = node_of(&batch, WidgetKind::Dropdown);
let props = props_of(&batch, node);
assert!(props.contains(&(PropertyKind::SelectedIndex, PropertyValue::Integer(1))));
let children = decode_batch(&batch)
.unwrap()
.into_iter()
.filter(|mutation| {
matches!(mutation, Mutation::Insert { parent_id, node_id, .. }
if *parent_id == node && *node_id != 0)
})
.count();
assert_eq!(children, 2);
let handler = handler_of(&batch, node, PropertyKind::OnValueChange);
let mut encoded = Vec::new();
encode_event(
&HostEvent {
node_id: node,
handler_id: handler,
payload: EventPayload::ValueChanged(1.0),
},
&mut encoded,
)
.unwrap();
host.dispatch_event(&encoded).unwrap();
PICKED.with(|picked| assert_eq!(*picked.borrow(), vec![1]));
}
#[test]
fn fr15_no_property_lets_the_host_choose_how_a_picker_is_operated() {
let named: Vec<_> = compose_rust::schema::PROPERTY_SCHEMA
.iter()
.map(|property| property.name.to_ascii_lowercase())
.filter(|name| {
[
"wheel", "dial", "calendar", "grid", "flyout", "mode", "style", "format",
]
.iter()
.any(|banned| name.contains(banned))
})
.collect();
assert!(
named.is_empty(),
"a picker property named how the user picks, or how the value is formatted: {named:?}. \
The design systems are operated differently, and a Host that can ask for one \
of them turns the rest into imitations of it.",
);
}
#[test]
fn fr15_pickers_emit_no_appearance_of_their_own() {
let mut host = Host::new(date_app);
let batch = host.rebuild().unwrap().to_vec();
let node = node_of(&batch, WidgetKind::DatePicker);
let unexpected: Vec<_> = props_of(&batch, node)
.into_iter()
.map(|(property, _)| property)
.filter(|property| {
!matches!(
property,
PropertyKind::Value
| PropertyKind::Min
| PropertyKind::Max
| PropertyKind::Enabled
| PropertyKind::OnValueChange
)
})
.collect();
assert!(unexpected.is_empty(), "a picker sent {unexpected:?}");
}