use teksilo_canvas::SizeProposal;
use teksilo_core::event::{Key, Modifiers};
use teksilo_core::signal::Signal;
use teksilo_core::widget_tree::WidgetTree;
use teksilo_i18n::lit;
use super::{SpinBox, StepType, WrapMode};
fn tick(tree: &mut WidgetTree) {
tree.request_frame();
tree.tick_animations(std::time::Duration::from_millis(16));
tree.layout(SizeProposal::exact(300.0, 60.0));
}
fn setup_int(
initial: i32,
min: i32,
max: i32,
) -> (WidgetTree, Signal<i32>, teksilo_core::widget_id::WidgetId) {
let value = Signal::new(initial);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), min, max));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
(tree, value, id)
}
fn focus_field(tree: &mut WidgetTree, spin_id: teksilo_core::widget_id::WidgetId) {
let field = tree
.first_focusable_descendant(spin_id)
.expect("SpinBox should have a focusable inner field");
tree.focus(field);
}
#[test]
fn constructs_and_lays_out() {
let (tree, _v, id) = setup_int(0, 0, 100);
let bounds = tree.bounds(id);
assert!(bounds.width > 0.0);
assert!(bounds.height > 0.0);
}
#[test]
fn width_grows_with_text_scale() {
let value = Signal::new(50);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value, 0, 100).width_chars(3).suffix(" %"));
tree.layout(SizeProposal::unspecified());
let w1 = tree.bounds(id).width;
tree.set_user_text_scale(2.0);
tree.layout(SizeProposal::unspecified());
let w2 = tree.bounds(id).width;
assert!(
w2 > w1 * 1.4,
"spinbox width should grow with the text scale: {w1} -> {w2}"
);
}
#[test]
fn arrow_up_increments() {
let (mut tree, value, id) = setup_int(10, 0, 100);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 11);
}
#[test]
fn arrow_down_decrements() {
let (mut tree, value, id) = setup_int(10, 0, 100);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowDown, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 9);
}
#[test]
fn page_up_uses_page_step() {
let value = Signal::new(10_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 100).page_step(25));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::PageUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 35);
}
#[test]
fn page_step_defaults_to_ten_times_single_step() {
let value = Signal::new(10_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 1000).single_step(3));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::PageUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 40, "page step default must be 10x single step");
}
#[test]
fn clamp_mode_blocks_past_max() {
let (mut tree, value, id) = setup_int(99, 0, 100);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
tree.press_key(Key::ArrowUp, Modifiers::NONE); tick(&mut tree);
assert_eq!(value.get(), 100);
}
#[test]
fn clamp_mode_blocks_below_min() {
let (mut tree, value, id) = setup_int(1, 0, 100);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowDown, Modifiers::NONE);
tick(&mut tree);
tree.press_key(Key::ArrowDown, Modifiers::NONE); tick(&mut tree);
assert_eq!(value.get(), 0);
}
#[test]
fn wrap_mode_wraps_past_max() {
let value = Signal::new(9_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 9).wrap_mode(WrapMode::Wrap));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 0, "wrap past max jumps to min");
}
#[test]
fn wrap_mode_wraps_past_min() {
let value = Signal::new(0_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 9).wrap_mode(WrapMode::Wrap));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowDown, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 9, "wrap past min jumps to max");
}
#[test]
fn read_only_blocks_keyboard_step() {
let value = Signal::new(10_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 100).read_only(true));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 10, "read_only must block stepping");
}
#[test]
fn adaptive_step_scales_to_magnitude() {
let value = Signal::new(250_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
SpinBox::new(value.clone(), 0, 10_000)
.single_step(1)
.step_type(StepType::Adaptive),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 350);
}
#[test]
fn adaptive_step_small_values_use_base_step() {
let value = Signal::new(3_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
SpinBox::new(value.clone(), 0, 1000)
.single_step(1)
.step_type(StepType::Adaptive),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 4, "adaptive under 10 should keep base step");
}
#[test]
fn external_value_set_reformats_text() {
let value = Signal::new(0_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let _id = tree.add(SpinBox::new(value.clone(), 0, 100));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
value.set(42);
tick(&mut tree);
tick(&mut tree);
assert_eq!(value.get(), 42);
}
#[test]
fn a11y_role_is_spin_button() {
let (tree, _v, id) = setup_int(50, 0, 100);
let info = tree.accessibility_node(id);
assert_eq!(info.role(), teksilo_core::accesskit::Role::SpinButton);
let actions = info.actions();
assert!(actions.contains(&teksilo_core::accesskit::Action::Increment));
assert!(actions.contains(&teksilo_core::accesskit::Action::Decrement));
assert!(actions.contains(&teksilo_core::accesskit::Action::Focus));
}
#[test]
fn disabled_blocks_keyboard_step() {
let value = Signal::new(10_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 100).enabled(false));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
if let Some(field) = tree.first_focusable_descendant(id) {
tree.focus(field);
}
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 10, "disabled SpinBox must not step");
}
#[test]
fn float_type_formats_with_decimals() {
let value = Signal::new(0.25_f64);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let _id = tree.add(
SpinBox::new(value.clone(), 0.0, 1.0)
.single_step(0.05)
.decimals(2),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
assert!((value.get() - 0.25).abs() < 1e-9);
}
#[test]
fn float_arrow_steps_by_single_step() {
let value = Signal::new(0.5_f32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
SpinBox::new(value.clone(), 0.0, 1.0)
.single_step(0.1)
.decimals(2),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert!((value.get() - 0.6).abs() < 1e-5, "got {}", value.get());
}
#[test]
fn on_value_changed_fires_on_step() {
use std::cell::Cell;
use std::rc::Rc;
let value = Signal::new(0_i32);
let fired = Rc::new(Cell::new(0_i32));
let c = fired.clone();
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id =
tree.add(SpinBox::new(value.clone(), 0, 100).on_value_changed(move |v, _ctx| c.set(v)));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(fired.get(), 1);
}
#[test]
fn hidden_buttons_still_step_via_keyboard() {
use super::ButtonLayout;
let value = Signal::new(10_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 100).button_layout(ButtonLayout::Hidden));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
focus_field(&mut tree, id);
tree.press_key(Key::ArrowUp, Modifiers::NONE);
tick(&mut tree);
assert_eq!(value.get(), 11);
}
#[test]
fn show_buttons_sugar_matches_button_layout() {
use super::ButtonLayout;
let a = Signal::new(0_i32);
let b = Signal::new(0_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let ia = tree.add(SpinBox::new(a.clone(), 0, 10).show_buttons(false));
let ib = tree.add(SpinBox::new(b.clone(), 0, 10).button_layout(ButtonLayout::Hidden));
tree.layout(SizeProposal::exact(300.0, 120.0));
tick(&mut tree);
fn count_focusable(tree: &WidgetTree, root: teksilo_core::widget_id::WidgetId) -> usize {
let mut count = 0;
if tree.first_focusable_descendant(root).is_some() {
count += 1;
}
count
}
assert_eq!(count_focusable(&tree, ia), count_focusable(&tree, ib));
}
#[test]
fn a11y_numeric_value_matches_signal() {
let value = Signal::new(42_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value.clone(), 0, 100).single_step(2));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
let info = tree.accessibility_node(id);
assert_eq!(info.role(), teksilo_core::accesskit::Role::SpinButton);
let actions = info.actions();
for required in [
teksilo_core::accesskit::Action::Increment,
teksilo_core::accesskit::Action::Decrement,
teksilo_core::accesskit::Action::SetValue,
teksilo_core::accesskit::Action::Focus,
] {
assert!(
actions.contains(&required),
"missing a11y action {:?}",
required
);
}
}
#[test]
fn a11y_name_uses_label() {
let value = Signal::new(0_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value, 0, 100).label(lit!("Font size")));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
let info = tree.accessibility_node(id);
assert_eq!(info.name(), Some("Font size"));
}
#[test]
fn reactive_suffix_survives_value_transitions() {
let value = Signal::new(0_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
SpinBox::new(value.clone(), 0, 3600)
.suffix(" s")
.special_value_text(lit!("Never"))
.label(lit!("Timeout")),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
value.set(30);
tick(&mut tree);
value.set(0);
tick(&mut tree);
value.set(120);
tick(&mut tree);
assert_eq!(value.get(), 120);
let info = tree.accessibility_node(id);
assert_eq!(info.role(), teksilo_core::accesskit::Role::SpinButton);
}
#[test]
fn tooltip_appears_on_hover() {
let value = Signal::new(0_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value, 0, 100).tooltip(lit!("Tip")));
tree.layout(SizeProposal::exact(300.0, 60.0));
tree.pointer_move(tree.bounds(id).center());
tree.advance_time(std::time::Duration::from_secs(1));
assert_eq!(
tree.active_overlays().len(),
1,
"tooltip should appear on hover"
);
assert!(tree.find_by_label("Tip").is_some());
}
fn frame_colors(
tree: &mut WidgetTree,
id: teksilo_core::widget_id::WidgetId,
) -> (Option<[f32; 4]>, Option<[f32; 4]>) {
let b = tree.bounds(id);
let covers = |s: &[f32; 4]| {
(s[0] - b.x).abs() < 0.5
&& (s[1] - b.y).abs() < 0.5
&& (s[2] - b.width).abs() < 0.5
&& (s[3] - b.height).abs() < 0.5
};
let frame = tree.render();
let fill = frame
.shapes
.iter()
.find(|s| covers(&s.screen) && s.stroke_width == 0.0)
.map(|s| s.color);
let border = frame
.shapes
.iter()
.find(|s| covers(&s.screen) && s.stroke_width > 0.0)
.map(|s| s.color);
(fill, border)
}
fn assert_color(got: Option<[f32; 4]>, want: teksilo_tokens::Color, what: &str) {
let want = want.to_array();
let got = got.unwrap_or_else(|| panic!("no {what} quad painted at the SpinBox bounds"));
assert!(
got.iter()
.zip(want.iter())
.all(|(a, b)| (a - b).abs() < 1e-4),
"{what}: expected {want:?}, painted {got:?}"
);
}
fn spin_box_tree(enabled: bool) -> (WidgetTree, teksilo_core::widget_id::WidgetId) {
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(Signal::new(5_i32), 0, 100).enabled(enabled));
tree.layout(SizeProposal::exact(300.0, 60.0));
(tree, id)
}
#[test]
fn disabled_spin_box_paints_the_neutral_disabled_frame() {
let theme = teksilo_core::presets::intui::light();
let (mut tree, id) = spin_box_tree(false);
let (fill, border) = frame_colors(&mut tree, id);
assert_color(fill, theme.colors.surface_disabled, "fill");
assert_color(border, theme.colors.border_disabled, "border");
let accent_disabled = theme.colors.accent_disabled.to_array();
assert_ne!(fill.unwrap(), accent_disabled);
assert_ne!(border.unwrap(), accent_disabled);
}
#[test]
fn enabled_spin_box_frame_is_unchanged() {
let theme = teksilo_core::presets::intui::light();
let (mut tree, id) = spin_box_tree(true);
let (fill, border) = frame_colors(&mut tree, id);
assert_color(fill, theme.colors.surface_content, "fill");
assert_color(border, theme.colors.border, "border");
}
#[test]
fn spin_box_dims_reactively_without_a_rebuild() {
let theme = teksilo_core::presets::intui::light();
let enabled = Signal::new(true);
let mut tree = WidgetTree::new().with_theme(theme.clone());
let id = tree.add(SpinBox::new(Signal::new(5_i32), 0, 100).enabled(enabled.clone()));
tree.layout(SizeProposal::exact(300.0, 60.0));
let (fill, _) = frame_colors(&mut tree, id);
assert_color(fill, theme.colors.surface_content, "fill (enabled)");
enabled.set(false);
tree.layout(SizeProposal::exact(300.0, 60.0));
let (fill, border) = frame_colors(&mut tree, id);
assert_color(fill, theme.colors.surface_disabled, "fill (after disable)");
assert_color(
border,
theme.colors.border_disabled,
"border (after disable)",
);
}
#[test]
fn spin_box_dims_inside_a_disabled_ancestor() {
use crate::primitives::VStack;
let theme = teksilo_core::presets::intui::light();
let enabled = Signal::new(true);
let mut tree = WidgetTree::new().with_theme(theme.clone());
let form = tree.add(VStack::new().child(SpinBox::new(Signal::new(5_i32), 0, 100)));
tree.enabled_when(form, enabled.clone());
tree.layout(SizeProposal::exact(300.0, 60.0));
let spin = tree
.children(form)
.first()
.copied()
.expect("VStack should hold the SpinBox");
enabled.set(false);
tree.layout(SizeProposal::exact(300.0, 60.0));
let (fill, _) = frame_colors(&mut tree, spin);
assert_color(fill, theme.colors.surface_disabled, "fill");
}
fn wheel(tree: &mut WidgetTree, spin_id: teksilo_core::widget_id::WidgetId, lines: f32) {
use teksilo_canvas::Point;
use teksilo_core::event::{ScrollDelta, WidgetEvent};
let b = tree.bounds(spin_id);
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(b.x + b.width * 0.5, b.y + b.height * 0.5),
});
tree.dispatch_event(WidgetEvent::Scroll {
delta: ScrollDelta::Lines { x: 0.0, y: lines },
modifiers: Modifiers::NONE,
});
tick(tree);
}
fn hover_wheel_spin(initial: i32) -> (WidgetTree, Signal<i32>, teksilo_core::widget_id::WidgetId) {
let value = Signal::new(initial);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
SpinBox::new(value.clone(), 0, 100)
.single_step(1)
.wheel_mode(super::WheelMode::Hover),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
(tree, value, id)
}
#[test]
fn wheel_down_decrements_and_wheel_up_increments() {
let (mut tree, value, id) = hover_wheel_spin(50);
wheel(&mut tree, id, 3.0);
assert_eq!(value.get(), 49, "wheel down must decrease the value");
wheel(&mut tree, id, -3.0);
assert_eq!(value.get(), 50, "wheel up must increase the value");
}
#[test]
fn the_wheel_agrees_with_the_arrow_keys() {
let (mut tree, value, id) = hover_wheel_spin(10);
wheel(&mut tree, id, 3.0);
assert_eq!(value.get(), 9, "one wheel-down notch is one step down");
focus_field(&mut tree, id);
tree.press_key(Key::ArrowDown, Modifiers::NONE);
tick(&mut tree);
assert_eq!(
value.get(),
8,
"ArrowDown must move the same direction as wheel down"
);
}
#[test]
fn wheel_mode_disabled_ignores_the_notch() {
let value = Signal::new(50_i32);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
SpinBox::new(value.clone(), 0, 100)
.single_step(1)
.wheel_mode(super::WheelMode::Disabled),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
wheel(&mut tree, id, 3.0);
assert_eq!(value.get(), 50);
}
fn at_value(tree: &mut WidgetTree, id: teksilo_core::widget_id::WidgetId) -> String {
let update = tree.sync_accessibility();
let target = teksilo_core::accessibility::widget_id_to_node_id(id);
update
.nodes
.iter()
.find(|(nid, _)| *nid == target)
.and_then(|(_, n)| n.value().map(str::to_string))
.expect("spin box AT value")
}
fn with_locale<R>(tag: &str, f: impl FnOnce() -> R) -> R {
teksilo_i18n::thread_local::clear();
let cfg = teksilo_i18n::I18nConfig::test_only(tag, &[("x", "x")]);
teksilo_i18n::thread_local::install(teksilo_i18n::I18nManager::from_config(&cfg));
let out = f();
teksilo_i18n::thread_local::clear();
out
}
#[test]
fn displays_the_locale_decimal_separator() {
with_locale("fr-FR", || {
let value = Signal::new(12.5_f64);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value, 0.0, 100.0).decimals(1));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
assert_eq!(at_value(&mut tree, id), "12,5");
});
}
#[test]
fn grouping_is_off_by_default_and_opt_in() {
with_locale("en-US", || {
let value = Signal::new(1_234_567_i64);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let plain = tree.add(SpinBox::new(value.clone(), 0, 9_999_999));
let grouped = tree.add(SpinBox::new(value, 0, 9_999_999).use_grouping(true));
tree.layout(SizeProposal::exact(600.0, 200.0));
tick(&mut tree);
assert_eq!(at_value(&mut tree, plain), "1234567");
assert_eq!(at_value(&mut tree, grouped), "1,234,567");
});
}
#[test]
fn localized_false_pins_the_c_locale() {
with_locale("fr-FR", || {
let value = Signal::new(8080.5_f64);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(
SpinBox::new(value, 0.0, 99999.0)
.decimals(1)
.localized(false),
);
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
assert_eq!(at_value(&mut tree, id), "8080.5");
});
}
#[test]
fn grouping_keeps_large_integers_exact() {
with_locale("en-US", || {
let value = Signal::new(9_007_199_254_740_993_i64);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
let id = tree.add(SpinBox::new(value, 0, i64::MAX).use_grouping(true));
tree.layout(SizeProposal::exact(400.0, 60.0));
tick(&mut tree);
assert_eq!(at_value(&mut tree, id), "9,007,199,254,740,993");
});
}
#[test]
fn a_locale_switch_re_renders_the_number_in_place() {
with_locale("en-US", || {
let value = Signal::new(12.5_f64);
let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
tree.set_locale("en-US".to_string());
let id = tree.add(SpinBox::new(value, 0.0, 100.0).decimals(1));
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
assert_eq!(at_value(&mut tree, id), "12.5");
teksilo_i18n::thread_local::clear();
let cfg = teksilo_i18n::I18nConfig::test_only("fr-FR", &[("x", "x")]);
teksilo_i18n::thread_local::install(teksilo_i18n::I18nManager::from_config(&cfg));
tree.set_locale("fr-FR".to_string());
tree.layout(SizeProposal::exact(300.0, 60.0));
tick(&mut tree);
assert_eq!(at_value(&mut tree, id), "12,5");
});
}
#[test]
fn the_commit_path_reads_the_locale_form_back() {
with_locale("fr-FR", || {
let p = super::NumberPresentation::resolve(true, false);
assert_eq!(p.parse::<f64>("12,5"), Some(12.5));
assert_eq!(p.parse::<f64>("-12,5"), Some(-12.5));
assert_eq!(p.parse::<f64>("12.5"), Some(12.5));
assert_eq!(p.parse::<f64>("nope"), None);
});
}
#[test]
fn the_commit_path_reads_grouped_input() {
with_locale("en-US", || {
let p = super::NumberPresentation::resolve(true, true);
assert_eq!(p.parse::<i64>("1,234,567"), Some(1_234_567));
assert_eq!(
p.parse::<i64>("9,007,199,254,740,993"),
Some(9_007_199_254_740_993)
);
});
}
#[test]
fn the_input_filter_admits_the_locale_separator_and_ascii_both() {
with_locale("fr-FR", || {
let p = super::NumberPresentation::resolve(true, false);
assert!(p.accepts_char::<f64>(','), "the locale decimal separator");
assert!(p.accepts_char::<f64>('.'), "the numeric keypad dot");
assert!(p.accepts_char::<f64>('-'));
assert!(p.accepts_char::<f64>('7'));
assert!(!p.accepts_char::<f64>('q'));
});
with_locale("ar-EG", || {
let p = super::NumberPresentation::resolve(true, false);
assert!(p.accepts_char::<f64>('٧'), "an Arabic-Indic digit");
assert!(p.accepts_char::<f64>('٫'), "the locale decimal separator");
assert!(p.accepts_char::<f64>('7'), "an ASCII digit still types");
});
}
#[test]
fn the_input_filter_admits_the_group_separator_only_when_grouping() {
with_locale("fr-FR", || {
let ungrouped = super::NumberPresentation::resolve(true, false);
let grouped = super::NumberPresentation::resolve(true, true);
assert!(!ungrouped.accepts_char::<f64>('\u{202f}'));
assert!(grouped.accepts_char::<f64>('\u{202f}'));
});
}
#[test]
fn localized_false_neither_renders_nor_reads_the_locale_form() {
with_locale("fr-FR", || {
let p = super::NumberPresentation::resolve(false, false);
assert_eq!(p.parse::<f64>("12.5"), Some(12.5));
assert_eq!(p.parse::<f64>("12,5"), None);
assert!(!p.accepts_char::<f64>(','));
});
}