#[test]
fn full_pipeline_minimal() {
let yaml = include_str!("fixtures/minimal.yaml");
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("fn create_main"));
}
#[test]
fn full_pipeline_rejects_invalid() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
styles: ghost
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err();
assert!(err.to_string().contains("ghost"));
}
#[test]
fn full_pipeline_label_styles() {
let yaml = include_str!("fixtures/label_styles.yaml");
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("// SPDX-License-Identifier: GPL-3.0-only"));
assert!(code.contains("style_heading"));
assert!(code.contains("text(\"Title\")"));
}
#[test]
fn generate_file_to_dir() {
let dir = std::env::temp_dir().join("oxiforge_test");
std::fs::create_dir_all(&dir).unwrap();
let yaml_path = std::path::Path::new("tests/fixtures/minimal.yaml");
oxiforge::generate(yaml_path, &dir).unwrap();
let out = dir.join("minimal.rs");
assert!(out.exists());
let code = std::fs::read_to_string(&out).unwrap();
assert!(code.contains("fn create_main"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn generates_full_dashboard() {
let yaml = include_str!("fixtures/dashboard.yaml");
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("fn create_dashboard"));
assert!(code.contains("fn create_settings"));
assert!(code.contains("style_card"));
assert!(code.contains("style_value_text"));
assert!(code.contains("Arc::new"));
assert!(code.contains("Switch::new"));
assert!(code.contains("Slider::new"));
assert!(code.contains("Dropdown::new"));
}
#[test]
fn suggests_bg_color_for_background_color() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
background_color: 0xFF0000
text: "hi"
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("background_color"));
assert!(err.contains("bg_color"));
}
#[test]
fn generates_arc_gauge() {
let yaml = include_str!("fixtures/arc_gauge.yaml");
let code = oxiforge::generate_from_str(yaml).unwrap();
eprintln!("{code}");
assert!(code.contains("fn create_meter_view"));
assert!(code.contains("Arc::new"));
assert!(code.contains("set_range(150.0)"));
assert!(code.contains("set_rotation(130)"));
assert!(code.contains("style_current_arc_track"));
assert!(code.contains("pub struct MeterViewWidgets"));
assert!(code.contains("pub current_arc: Arc<'static>"));
assert!(code.contains("ValueLabel::new"));
assert!(code.contains("pub field_voltage: ValueLabel<'static>"));
assert!(code.contains("FONT_LV_FONT_ALTREG_RPM"));
}
#[test]
fn struct_return_for_id_widgets() {
let yaml = r#"
lvgl:
pages:
- id: gauges
widgets:
- arc:
id: main_arc
min_value: 0.0
max_value: 100.0
value: 0.0
- obj:
width: 100
height: 100
widgets:
- label:
id: value_text
text: "0"
- label:
text: "anon"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
eprintln!("{code}");
assert!(code.contains("pub struct GaugesWidgets"));
assert!(code.contains("pub main_arc: Arc<'static>"));
assert!(code.contains("pub value_text: Label<'static>"));
assert!(code.contains("-> Result<GaugesWidgets, WidgetError>"));
assert!(!code.contains("core::mem::forget"));
assert!(code.contains("_retained_1:"));
}
#[test]
fn decorative_only_page_emits_widgets_struct() {
let yaml = r#"
lvgl:
pages:
- id: splash
widgets:
- label:
text: "Loading..."
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("pub struct SplashWidgets"));
assert!(code.contains("_retained_1: Label<'static>"));
assert!(code.contains("-> Result<SplashWidgets, WidgetError>"));
assert!(!code.contains("core::mem::forget"));
}
#[test]
fn value_label_widget() {
let yaml = r#"
lvgl:
pages:
- id: dash
widgets:
- value_label:
id: voltage
text: "-.-V"
unit: "V"
align: top_left
text_color: 0x6080a0
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("ValueLabel::new"));
assert!(code.contains(r#""V""#));
assert!(code.contains(r#"text("-.-V")"#));
}
#[test]
fn custom_font_reference() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
id: rpm
text: "0 rpm"
text_font: custom(lv_font_altreg_rpm)
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("unsafe extern \"C\""));
assert!(code.contains("static lv_font_altreg_rpm: u8"));
assert!(code.contains("static FONT_LV_FONT_ALTREG_RPM: Font"));
assert!(code.contains("Font::from_extern"));
assert!(code.contains("font(FONT_LV_FONT_ALTREG_RPM)"));
}
#[test]
fn custom_font_in_style_def() {
let yaml = r#"
lvgl:
style_definitions:
- id: rpm_style
text_font: custom(lv_font_altreg_rpm)
pages:
- id: main
widgets:
- label:
text: "hi"
styles: rpm_style
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("static lv_font_altreg_rpm: u8"));
assert!(code.contains("FONT_LV_FONT_ALTREG_RPM"));
}
#[test]
fn suggests_state_pressed_for_pressed() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
pressed:
bg_color: 0x0000FF
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("state_pressed"));
}
#[test]
fn button_widget_with_id() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
id: my_btn
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("Button::new"));
assert!(code.contains("pub my_btn: Button<'static>"));
}
#[test]
fn roller_widget() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- roller:
options: ["A", "B", "C"]
visible_row_count: 3
selected: 1
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("Roller::new"));
assert!(code.contains("RollerMode::Normal"));
assert!(code.contains("set_visible_row_count(3)"));
assert!(code.contains("set_selected(1, false)"));
}
#[test]
fn screen_text_color_and_flex_align() {
let yaml = r#"
lvgl:
pages:
- id: main
text_color: 0xAABBCC
layout: flex
flex_flow: column
flex_align_main: center
flex_align_cross: end
flex_align_track: space_between
widgets:
- label:
text: "hi"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("b.text_color_hex(0xAABBCC)"));
assert!(code.contains("container.add_style(&container_inline, Selector::DEFAULT)"));
assert!(code.contains("FlexAlign::End"));
assert!(code.contains("FlexAlign::SpaceBetween"));
}
#[test]
fn style_def_opa_and_padding() {
let yaml = r#"
lvgl:
style_definitions:
- id: padded
bg_opa: 200
pad_top: 10
pad_left: 5
pad_right: 5
text_opa: 128
pages:
- id: main
widgets:
- label:
text: "x"
styles: padded
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains(".bg_opa(200)"));
assert!(code.contains(".pad_top(10 as i32)"));
assert!(code.contains(".pad_left(5 as i32)"));
assert!(code.contains(".pad_right(5 as i32)"));
assert!(code.contains(".text_opa(128)"));
}
#[test]
fn named_color_in_widget() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
text: "red"
text_color: red
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("Palette::red()"));
}
#[test]
fn named_opacity_transp_and_cover() {
let yaml = r#"
lvgl:
style_definitions:
- id: s1
bg_opa: "transp"
- id: s2
bg_opa: "cover"
pages:
- id: main
widgets:
- label:
text: "a"
styles: s1
- label:
text: "b"
styles: s2
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains(".bg_opa(0)"));
assert!(code.contains(".bg_opa(255)"));
}
#[test]
fn size_content_dimension() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
width: "size_content"
height: 50
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("LV_SIZE_CONTENT"));
}
#[test]
fn flex_align_space_evenly_and_around() {
let yaml = r#"
lvgl:
pages:
- id: pg1
widgets:
- obj:
layout: flex
flex_flow: row
flex_align_main: space_evenly
- obj:
layout: flex
flex_flow: row
flex_align_main: space_around
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("FlexAlign::SpaceEvenly"));
assert!(code.contains("FlexAlign::SpaceAround"));
}
#[test]
fn border_side_variants() {
let yaml = r#"
lvgl:
style_definitions:
- id: s_none
border_side: none
- id: s_bottom
border_side: bottom
- id: s_left
border_side: left
- id: s_right
border_side: right
- id: s_full
border_side: full
pages:
- id: main
widgets:
- label:
text: "a"
styles: s_none
- label:
text: "b"
styles: s_bottom
- label:
text: "c"
styles: s_left
- label:
text: "d"
styles: s_right
- label:
text: "e"
styles: s_full
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("BorderSide::NONE"));
assert!(code.contains("BorderSide::BOTTOM"));
assert!(code.contains("BorderSide::LEFT"));
assert!(code.contains("BorderSide::RIGHT"));
assert!(code.contains("BorderSide::FULL"));
}
#[test]
fn text_decor_variants() {
let yaml = r#"
lvgl:
style_definitions:
- id: s_none
text_decor: none
- id: s_strike
text_decor: strikethrough
pages:
- id: main
widgets:
- label:
text: "a"
styles: s_none
- label:
text: "b"
styles: s_strike
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("TextDecor::NONE"));
assert!(code.contains("TextDecor::STRIKETHROUGH"));
}
#[test]
fn custom_font_invalid_falls_back() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
text: "a"
text_font: "custom()"
- label:
text: "b"
text_font: "custom(foo bar)"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("oxivgl::fonts::CUSTOM()"));
assert!(code.contains("oxivgl::fonts::CUSTOM(FOO BAR)"));
}
#[test]
fn pascal_case_leading_underscore() {
let yaml = r#"
lvgl:
pages:
- id: _test
widgets:
- label:
text: "x"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("fn create__test"));
}
#[test]
fn css_hex_3_char() {
let yaml = r##"
lvgl:
pages:
- id: main
widgets:
- label:
text: "r"
text_color: "#F00"
"##;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("0xFF0000"));
}
#[test]
fn invalid_css_hex_color() {
let yaml = r##"
lvgl:
pages:
- id: main
widgets:
- label:
text: "x"
text_color: "#GGGGGG"
"##;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("hex"));
}
#[test]
fn invalid_dimension_string() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
width: "banana"
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("invalid dimension"));
}
#[test]
fn opacity_as_integer() {
let yaml = r#"
lvgl:
style_definitions:
- id: s1
bg_opa: 128
pages:
- id: main
widgets:
- label:
text: "x"
styles: s1
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains(".bg_opa(128)"));
}
#[test]
fn track_size_as_integer() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
layout: grid
grid_columns: [100, 200]
grid_rows: ["1fr"]
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("100"));
assert!(code.contains("200"));
}
#[test]
fn invalid_track_size() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- obj:
layout: grid
grid_columns: ["banana"]
grid_rows: ["1fr"]
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("invalid track size"));
}
#[test]
fn unknown_property_no_suggestion() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
zzzzqqqxxx: 42
text: "x"
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("unknown property 'zzzzqqqxxx'"));
assert!(!err.contains("did you mean"));
}
#[test]
fn broken_yaml_hints_no_crash() {
let yaml = r#"{{{{not valid yaml at all"#;
let err = oxiforge::generate_from_str(yaml);
assert!(err.is_err());
}
#[test]
fn generate_file_rejects_unknown_key() {
let dir = std::env::temp_dir().join("oxiforge_test_hint");
std::fs::create_dir_all(&dir).unwrap();
let yaml_path = dir.join("bad_hint.yaml");
std::fs::write(
&yaml_path,
r#"
lvgl:
pages:
- id: main
widgets:
- label:
background_color: 0xFF0000
text: "x"
"#,
)
.unwrap();
let err = oxiforge::generate(&yaml_path, &dir).unwrap_err().to_string();
assert!(err.contains("background_color"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn generate_file_rejects_undefined_style() {
let dir = std::env::temp_dir().join("oxiforge_test_val");
std::fs::create_dir_all(&dir).unwrap();
let yaml_path = dir.join("bad_val.yaml");
std::fs::write(
&yaml_path,
r#"
lvgl:
pages:
- id: main
widgets:
- label:
text: "x"
styles: nonexistent
"#,
)
.unwrap();
let err = oxiforge::generate(&yaml_path, &dir).unwrap_err().to_string();
assert!(err.contains("nonexistent"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn scale_rotation_angle_range_tick_length() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- scale:
mode: round_inner
total_tick_count: 26
major_tick_every: 5
range_min: 0
range_max: 150
rotation: 130
angle_range: 200
major_tick_length: 14
minor_tick_length: 0
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("set_rotation(130)"));
assert!(code.contains("set_angle_range(200)"));
assert!(code.contains("set_tick_length(Part::Indicator, 14)"));
assert!(code.contains("set_tick_length(Part::Items, 0)"));
}
#[test]
fn generates_event_bindings_in_full_pipeline() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
id: start_btn
on:
clicked: on_start_button_clicked
- slider:
id: volume
on:
value_changed: on_volume_changed
long_pressed: on_volume_long_press
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("on(oxivgl::enums::EventCode::CLICKED, on_start_button_clicked)"));
assert!(code.contains("on(oxivgl::enums::EventCode::VALUE_CHANGED, on_volume_changed)"));
assert!(code.contains("on(oxivgl::enums::EventCode::LONG_PRESSED, on_volume_long_press)"));
}
#[test]
fn generates_pressed_released_event_bindings() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
id: nav_key
on:
pressed: nav_pressed
released: nav_released
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("on(oxivgl::enums::EventCode::PRESSED, nav_pressed)"));
assert!(code.contains("on(oxivgl::enums::EventCode::RELEASED, nav_released)"));
}
#[test]
fn full_pipeline_rejects_invalid_handler_name() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
on:
clicked: "my-handler"
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err();
assert!(err.to_string().contains("my-handler"));
}
#[test]
fn event_binding_emitted_before_struct_move() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
on:
clicked: on_btn
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
let on_pos = code.find(".on(oxivgl::enums::EventCode::CLICKED").unwrap();
let move_pos = code.find("_retained_1: _w1").unwrap();
assert!(on_pos < move_pos, ".on() call must appear before the widget is moved into the *Widgets struct");
}
#[test]
fn generates_view_single_page() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: main
widgets:
- label:
id: title
text: "hello"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("pub struct MainView"));
assert!(code.contains("widgets: Option<MainWidgets>"));
assert!(code.contains("impl oxivgl::view::View for MainView"));
assert!(code.contains("create_main(_container)"));
assert!(code.contains("fn will_hide(&mut self)"));
assert!(code.contains("pub type MainRootView = MainView;"));
}
#[test]
fn generates_view_multi_page() {
let yaml = r#"
lvgl:
generate_view: true
app_name: my_app
pages:
- id: dashboard
widgets:
- label:
id: lbl
text: "hi"
- id: settings
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("pub struct DashboardView"));
assert!(code.contains("pub struct SettingsView;"));
assert!(code.contains("impl oxivgl::view::View for DashboardView"));
assert!(code.contains("impl oxivgl::view::View for SettingsView"));
assert!(code.contains("pub type MyAppRootView = DashboardView;"));
}
#[test]
fn no_view_without_flag() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label:
id: lbl
text: "hi"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(!code.contains("impl oxivgl::view::View"));
assert!(!code.contains("RootView"));
}
#[test]
fn generates_view_with_update_and_event_hooks() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: main
update_fn: my_update
on_event_fn: my_on_event
widgets:
- label:
id: lbl
text: "hi"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("fn update(&mut self)"));
assert!(code.contains("my_update(&mut self.widgets)"));
assert!(code.contains("fn on_event(&mut self, event: &oxivgl::event::Event)"));
assert!(code.contains("my_on_event(&mut self.widgets, event)"));
}
#[test]
fn generates_declarative_input_group() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: menu
input_group:
members: [up, ok]
widgets:
- button:
id: up
- button:
id: ok
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("_input_group: Option<oxivgl::group::Group>"));
assert!(code.contains("Self { widgets: None, _input_group: None }"));
assert!(code.contains("let _group = oxivgl::group::Group::new()?;"));
assert!(code.contains("_group.add_obj(&widgets.up);"));
assert!(code.contains("_group.add_obj(&widgets.ok);"));
assert!(code.contains("self._input_group = Some(_group);"));
assert!(code.contains("self._input_group = None;"));
assert!(code.contains("fn input_group(&self) -> Option<oxivgl::group::GroupRef>"));
assert!(code.contains("self._input_group.as_ref().map(|g| g.as_ref())"));
let add_pos = code.find("_group.add_obj(&widgets.up)").unwrap();
let move_pos = code.find("self.widgets = Some(widgets);").unwrap();
assert!(add_pos < move_pos, "group must be built before widgets are moved into the view");
}
#[test]
fn generates_hook_input_group() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: menu
input_group_fn: build_menu_group
widgets:
- button:
id: up
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(!code.contains("_input_group"));
assert!(code.contains("fn input_group(&self) -> Option<oxivgl::group::GroupRef>"));
assert!(code.contains("build_menu_group(&self.widgets)"));
}
#[test]
fn generates_hook_input_group_no_widgets() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: menu
input_group_fn: build_menu_group
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("fn input_group(&self) -> Option<oxivgl::group::GroupRef>"));
assert!(code.contains("build_menu_group()"));
}
#[test]
fn rejects_input_group_with_both_forms() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: menu
input_group:
members: [up]
input_group_fn: build_menu_group
widgets:
- button:
id: up
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("mutually exclusive"), "got: {err}");
}
#[test]
fn rejects_input_group_unknown_member() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: menu
input_group:
members: [upp]
widgets:
- button:
id: up
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("not a widget id"), "got: {err}");
assert!(err.contains("did you mean 'up'"), "got: {err}");
}
#[test]
fn rejects_input_group_empty_members() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: menu
input_group:
members: []
widgets:
- button:
id: up
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("no members"), "got: {err}");
}
#[test]
fn rejects_input_group_fn_invalid_ident() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: menu
input_group_fn: "not an ident"
widgets:
- button:
id: up
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("not a valid Rust identifier"), "got: {err}");
}
#[test]
fn generates_timed_toast() {
let yaml = r#"
lvgl:
toasts:
- id: no_sd
duration_ms: 4000
widgets:
- label:
text: "No SD card"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("pub struct NoSdToast"));
assert!(code.contains("impl oxivgl::view::View for NoSdToast"));
assert!(code.contains("self.widgets = Some(create_no_sd(_container)?);"));
assert!(code.contains("fn will_hide(&mut self)"));
assert!(!code.contains("fn update") || !code.contains("NoSdToast")); assert!(code.contains("pub fn no_sd_toast() -> oxivgl::view::NavAction"));
assert!(code.contains(
"oxivgl::view::NavAction::show_toast(NoSdToast::default(), Some(core::time::Duration::from_millis(4000)))"
));
}
#[test]
fn generates_persistent_toast() {
let yaml = r#"
lvgl:
toasts:
- id: sticky
persistent: true
widgets:
- label:
text: "stays"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("oxivgl::view::NavAction::show_toast(StickyToast::default(), None)"));
}
#[test]
fn toast_default_duration_is_2500ms() {
let yaml = r#"
lvgl:
toasts:
- id: plain
widgets:
- label:
text: "hi"
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("Some(core::time::Duration::from_millis(2500))"));
}
#[test]
fn rejects_toast_id_colliding_with_page() {
let yaml = r#"
lvgl:
pages:
- id: dash
widgets:
- label: { id: l, text: "x" }
toasts:
- id: dash
widgets:
- label: { text: "y" }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("collides with a page id"), "got: {err}");
}
#[test]
fn rejects_toast_with_both_timing_fields() {
let yaml = r#"
lvgl:
toasts:
- id: t
duration_ms: 1000
persistent: true
widgets:
- label: { text: "x" }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("mutually exclusive"), "got: {err}");
}
#[test]
fn rejects_empty_toast() {
let yaml = r#"
lvgl:
toasts:
- id: empty
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("no widgets"), "got: {err}");
}
#[test]
fn toasts_only_document_is_valid() {
let yaml = r#"
lvgl:
toasts:
- id: hello
widgets:
- label: { text: "hi" }
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("pub fn hello_toast()"));
}
#[test]
fn generates_parameterized_toast() {
let yaml = r#"
lvgl:
toasts:
- id: status
params: [msg]
widgets:
- label:
bind_text: msg
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("msg: alloc::string::String,"));
assert!(code.contains("pub fn create_status(container: &oxivgl::widgets::Obj<'static>, msg: &str)"));
assert!(code.contains("_w1.text(msg);")); assert!(code.contains("pub fn new(msg: impl Into<alloc::string::String>) -> Self"));
assert!(code.contains("create_status(_container, &self.msg)"));
assert!(code.contains("pub fn status_toast(msg: impl Into<alloc::string::String>) -> oxivgl::view::NavAction"));
assert!(code.contains("StatusToast::new(msg)"));
}
#[test]
fn generates_multi_param_toast() {
let yaml = r#"
lvgl:
toasts:
- id: kv
params: [key, value]
widgets:
- label: { bind_text: key }
- label: { bind_text: value }
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("create_kv(container: &oxivgl::widgets::Obj<'static>, key: &str, value: &str)"));
assert!(
code.contains(
"pub fn kv_toast(key: impl Into<alloc::string::String>, value: impl Into<alloc::string::String>)"
)
);
assert!(code.contains("KvToast::new(key, value)"));
}
#[test]
fn generates_toast_presets() {
let yaml = r#"
lvgl:
toast_presets: [info, warn, error]
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("pub fn info_toast(msg: impl Into<alloc::string::String>)"));
assert!(code.contains("pub fn warn_toast(msg: impl Into<alloc::string::String>)"));
assert!(code.contains("pub fn error_toast(msg: impl Into<alloc::string::String>)"));
assert!(code.contains("b.bg_color_hex(0x2266CC)")); assert!(code.contains("b.bg_color_hex(0xCC7A00)")); assert!(code.contains("b.bg_color_hex(0xAA2222)")); assert!(code.contains("container.add_style(&container_inline, Selector::DEFAULT)"));
}
#[test]
fn rejects_bind_text_unknown_param() {
let yaml = r#"
lvgl:
toasts:
- id: t
params: [msg]
widgets:
- label: { bind_text: mgs }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("not a declared param"), "got: {err}");
assert!(err.contains("did you mean 'msg'"), "got: {err}");
}
#[test]
fn rejects_unused_param() {
let yaml = r#"
lvgl:
toasts:
- id: t
params: [msg]
widgets:
- label: { text: "static" }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("no bind_text uses it"), "got: {err}");
}
#[test]
fn rejects_text_and_bind_text() {
let yaml = r#"
lvgl:
toasts:
- id: t
params: [msg]
widgets:
- label: { text: "x", bind_text: msg }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("both 'text' and 'bind_text'"), "got: {err}");
}
#[test]
fn rejects_bind_text_on_page() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- label: { bind_text: msg }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("only valid inside a toast"), "got: {err}");
}
#[test]
fn preset_id_collides_with_page() {
let yaml = r#"
lvgl:
toast_presets: [info]
pages:
- id: info
widgets:
- label: { text: "x" }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("collides with a page id"), "got: {err}");
}
#[test]
fn generates_post_toast_helper() {
let yaml = r#"
lvgl:
toasts:
- id: saved
params: [name]
widgets:
- label: { bind_text: name }
- id: ready
widgets:
- label: { text: "Ready" }
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains("pub fn post_saved(name: impl Into<alloc::string::String>)"));
assert!(code.contains("oxivgl::navigator::post_toast_with(move || SavedToast::new(name),"));
assert!(code.contains("pub fn post_ready()"));
assert!(code.contains("oxivgl::navigator::post_toast_with(ReadyToast::default,"));
}
#[test]
fn generates_on_show_toast_binding() {
let yaml = r#"
lvgl:
toasts:
- id: saved
params: [name]
widgets:
- label: { bind_text: name }
pages:
- id: main
widgets:
- button:
on:
clicked: { show_toast: saved, args: ["Profile"] }
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains(r#".on(oxivgl::enums::EventCode::CLICKED, |_| post_saved("Profile"));"#));
}
#[test]
fn on_handler_still_works_as_string() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
on:
clicked: my_handler
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(code.contains(".on(oxivgl::enums::EventCode::CLICKED, my_handler);"));
}
#[test]
fn rejects_show_toast_unknown_toast() {
let yaml = r#"
lvgl:
pages:
- id: main
widgets:
- button:
on:
clicked: { show_toast: nope }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("not a defined toast"), "got: {err}");
}
#[test]
fn rejects_show_toast_arg_count_mismatch() {
let yaml = r#"
lvgl:
toasts:
- id: kv
params: [k, v]
widgets:
- label: { bind_text: k }
- label: { bind_text: v }
pages:
- id: main
widgets:
- button:
on:
clicked: { show_toast: kv, args: ["only-one"] }
"#;
let err = oxiforge::generate_from_str(yaml).unwrap_err().to_string();
assert!(err.contains("takes 2 arg(s) but 1 given"), "got: {err}");
}
#[test]
fn skipped_page_not_in_output() {
let yaml = r#"
lvgl:
generate_view: true
pages:
- id: main
widgets:
- label:
id: lbl
text: "hi"
- id: hidden_page
skip: true
"#;
let code = oxiforge::generate_from_str(yaml).unwrap();
assert!(!code.contains("create_hidden_page"));
assert!(!code.contains("HiddenPageView"));
assert!(code.contains("MainView"));
}