use dampen_core::ir::{AttributeValue, EventKind, InterpolatedPart, WidgetKind};
use dampen_core::parser::parse;
#[test]
fn test_parse_valid_simple() {
let xml = include_str!("fixtures/valid_simple.dampen");
let result = parse(xml);
assert!(result.is_ok(), "Should parse valid simple XML");
let doc = result.unwrap();
assert!(matches!(doc.root.kind, WidgetKind::Column));
assert_eq!(doc.root.attributes.len(), 2);
assert!(
matches!(doc.root.attributes.get("padding"), Some(AttributeValue::Static(s)) if s == "20")
);
assert!(
matches!(doc.root.attributes.get("spacing"), Some(AttributeValue::Static(s)) if s == "10")
);
assert_eq!(doc.root.children.len(), 2);
let text = &doc.root.children[0];
assert!(matches!(text.kind, WidgetKind::Text));
assert!(
matches!(text.attributes.get("value"), Some(AttributeValue::Static(s)) if s == "Hello, World!")
);
let button = &doc.root.children[1];
assert!(matches!(button.kind, WidgetKind::Button));
assert!(
matches!(button.attributes.get("label"), Some(AttributeValue::Static(s)) if s == "Click me")
);
assert_eq!(button.events.len(), 1);
assert!(matches!(button.events[0].event, EventKind::Click));
assert_eq!(button.events[0].handler, "handle_click");
}
#[test]
fn test_parse_valid_nested() {
let xml = include_str!("fixtures/valid_nested.dampen");
let result = parse(xml);
assert!(result.is_ok(), "Should parse nested XML");
let doc = result.unwrap();
assert!(matches!(doc.root.kind, WidgetKind::Column));
assert_eq!(doc.root.children.len(), 2);
let row = &doc.root.children[1];
assert!(matches!(row.kind, WidgetKind::Row));
assert_eq!(row.children.len(), 3);
let middle_col = &row.children[1];
assert!(matches!(middle_col.kind, WidgetKind::Column));
assert_eq!(middle_col.children.len(), 2);
}
#[test]
fn test_parse_invalid_syntax() {
let xml = include_str!("fixtures/invalid_syntax.dampen");
let result = parse(xml);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.span.line > 0);
assert!(err.span.column > 0);
}
#[test]
fn test_parse_empty_xml() {
let result = parse("");
assert!(result.is_err());
}
#[test]
fn test_parse_unknown_widget() {
let xml = r#"<?xml version="1.0"?>
<unknown>
<text value="test" />
</unknown>"#;
let result = parse(xml);
assert!(result.is_err());
}
#[test]
fn test_parse_button_with_events() {
let xml = r#"<?xml version="1.0"?>
<column>
<button label="Submit" on_click="submit" on_press="press" />
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
let button = &doc.root.children[0];
assert_eq!(button.events.len(), 2);
let events: Vec<_> = button
.events
.iter()
.map(|e| (e.event.clone(), e.handler.as_str()))
.collect();
assert!(events.contains(&(EventKind::Click, "submit")));
assert!(events.contains(&(EventKind::Press, "press")));
}
#[test]
fn test_parse_binding_expressions() {
let xml = r#"<text value="Total: {count} items" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
if let WidgetKind::Text = doc.root.kind {
if let Some(AttributeValue::Interpolated(parts)) = doc.root.attributes.get("value") {
assert_eq!(parts.len(), 3);
if let InterpolatedPart::Literal(lit) = &parts[0] {
assert_eq!(lit, "Total: ");
} else {
panic!("Expected literal");
}
if let InterpolatedPart::Binding(expr) = &parts[1] {
if let dampen_core::expr::Expr::FieldAccess(field) = &expr.expr {
assert_eq!(field.path, vec!["count"]);
} else {
panic!("Expected field access");
}
} else {
panic!("Expected binding");
}
if let InterpolatedPart::Literal(lit) = &parts[2] {
assert_eq!(lit, " items");
} else {
panic!("Expected literal");
}
} else {
panic!("Expected interpolated attribute");
}
} else {
panic!("Expected text widget");
}
}
#[test]
fn test_parse_simple_binding() {
let xml = r#"<text value="{count}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
if let WidgetKind::Text = doc.root.kind {
if let Some(AttributeValue::Binding(expr)) = doc.root.attributes.get("value") {
if let dampen_core::expr::Expr::FieldAccess(field) = &expr.expr {
assert_eq!(field.path, vec!["count"]);
} else {
panic!("Expected field access");
}
} else {
panic!("Expected binding attribute");
}
} else {
panic!("Expected text widget");
}
}
#[test]
fn test_binding_eval_with_method() {
use dampen_core::{
AttributeValue, BindingValue, InterpolatedPart, UiBindable, WidgetKind,
evaluate_binding_expr, parse,
};
#[derive(Debug, Clone, Default)]
struct Model {
items: Vec<String>,
}
impl UiBindable for Model {
fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
match path {
["items"] => Some(BindingValue::from_value(&self.items)),
_ => None,
}
}
fn available_fields() -> Vec<String> {
vec!["items".to_string()]
}
}
let xml = r#"<text value="Total: {items.len()} items" />"#;
let doc = parse(xml).unwrap();
let model = Model {
items: vec!["a".to_string(), "b".to_string()],
};
if let WidgetKind::Text = doc.root.kind {
if let Some(AttributeValue::Interpolated(parts)) = doc.root.attributes.get("value") {
let mut result = String::new();
for part in parts {
match part {
InterpolatedPart::Literal(literal) => result.push_str(literal),
InterpolatedPart::Binding(binding_expr) => {
match evaluate_binding_expr(binding_expr, &model) {
Ok(value) => result.push_str(&value.to_display_string()),
Err(_) => result.push_str("[error]"),
}
}
}
}
assert_eq!(result, "Total: 2 items");
}
}
}
#[test]
fn test_conditional_binding() {
use dampen_core::{BindingValue, UiBindable, evaluate_binding_expr};
#[derive(Debug, Clone, Default)]
struct Model {
items: Vec<String>,
}
impl UiBindable for Model {
fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
match path {
["items"] => Some(BindingValue::from_value(&self.items)),
_ => None,
}
}
fn available_fields() -> Vec<String> {
vec!["items".to_string()]
}
}
let xml = r#"<text value="{if items.len() == 0 then 'No items yet!' else ''}" />"#;
let doc = parse(xml).unwrap();
let model = Model { items: Vec::new() };
if let WidgetKind::Text = doc.root.kind {
if let Some(AttributeValue::Binding(binding_expr)) = doc.root.attributes.get("value") {
match evaluate_binding_expr(binding_expr, &model) {
Ok(value) => {
assert_eq!(value.to_display_string(), "No items yet!");
}
Err(e) => {
panic!("Evaluation error: {}", e.message);
}
}
}
}
}
#[test]
fn test_tokenizer_conditional() {
use dampen_core::expr::tokenize_binding_expr;
let expr = "if items.len() == 0 then 'No items yet!' else ''";
let result = tokenize_binding_expr(expr, 0, 1, 1);
match result {
Ok(tokens) => {
println!("Tokens: {:?}", tokens);
}
Err(e) => {
panic!("Tokenization error: {}", e);
}
}
}
#[test]
fn test_parse_all_widget_types() {
let xml = r#"<column>
<text value="Test" />
<button label="Click" on_click="handler" />
<text_input value="input" on_input="handler" />
<checkbox label="Check" on_toggle="handler" />
<slider min="0" max="100" value="50" on_change="handler" />
<pick_list options="A,B,C" on_select="handler" />
<toggler label="Toggle" on_toggle="handler" />
<image src="img.png" />
<svg src="icon.svg" />
<space />
<rule />
<container><text value="Container" /></container>
<scrollable><text value="Scroll" /></scrollable>
<stack><text value="Stack" /></stack>
<row><text value="Row" /></row>
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert_eq!(doc.root.children.len(), 15);
let kinds: Vec<_> = doc.root.children.iter().map(|c| c.kind.clone()).collect();
assert!(kinds.contains(&WidgetKind::Text));
assert!(kinds.contains(&WidgetKind::Button));
assert!(kinds.contains(&WidgetKind::TextInput));
assert!(kinds.contains(&WidgetKind::Checkbox));
assert!(kinds.contains(&WidgetKind::Slider));
assert!(kinds.contains(&WidgetKind::PickList));
assert!(kinds.contains(&WidgetKind::Toggler));
assert!(kinds.contains(&WidgetKind::Image));
assert!(kinds.contains(&WidgetKind::Svg));
assert!(kinds.contains(&WidgetKind::Space));
assert!(kinds.contains(&WidgetKind::Rule));
assert!(kinds.contains(&WidgetKind::Container));
assert!(kinds.contains(&WidgetKind::Scrollable));
assert!(kinds.contains(&WidgetKind::Stack));
assert!(kinds.contains(&WidgetKind::Row));
}
#[test]
fn test_parse_layout_attributes() {
let xml = r#"<column spacing="10" padding="20" width="300" height="200" align="center">
<text value="Test" />
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert_eq!(doc.root.attributes.len(), 5);
match doc.root.attributes.get("spacing") {
Some(AttributeValue::Static(v)) => assert_eq!(v, "10"),
_ => panic!("Expected static spacing"),
}
match doc.root.attributes.get("padding") {
Some(AttributeValue::Static(v)) => assert_eq!(v, "20"),
_ => panic!("Expected static padding"),
}
match doc.root.attributes.get("width") {
Some(AttributeValue::Static(v)) => assert_eq!(v, "300"),
_ => panic!("Expected static width"),
}
match doc.root.attributes.get("height") {
Some(AttributeValue::Static(v)) => assert_eq!(v, "200"),
_ => panic!("Expected static height"),
}
match doc.root.attributes.get("align") {
Some(AttributeValue::Static(v)) => assert_eq!(v, "center"),
_ => panic!("Expected static align"),
}
}
#[test]
fn test_parse_binding_in_layout_attributes() {
let xml = r#"<column spacing="{spacing_value}" padding="{padding_value}">
<text value="Test" />
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("spacing") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::FieldAccess(field) = &expr.expr {
assert_eq!(field.path, vec!["spacing_value"]);
} else {
panic!("Expected field access for spacing");
}
}
_ => panic!("Expected binding for spacing"),
}
match doc.root.attributes.get("padding") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::FieldAccess(field) = &expr.expr {
assert_eq!(field.path, vec!["padding_value"]);
} else {
panic!("Expected field access for padding");
}
}
_ => panic!("Expected binding for padding"),
}
}
#[test]
fn test_parse_widget_specific_attributes() {
let xml = r#"<column>
<text value="Test" size="24" weight="bold" />
<button label="Click" on_click="handler" enabled="true" />
<text_input value="input" placeholder="Enter..." on_input="handler" />
<checkbox label="Check" checked="true" on_toggle="handler" />
<slider min="0" max="100" value="50" on_change="handler" />
<pick_list options="A,B,C" selected="B" on_select="handler" />
<toggler label="Toggle" active="true" on_toggle="handler" />
<image src="logo.png" width="100" height="50" />
<svg src="icon.svg" />
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
let text = &doc.root.children[0];
assert!(text.attributes.contains_key("value"));
assert!(text.attributes.contains_key("size"));
assert!(text.attributes.contains_key("weight"));
let button = &doc.root.children[1];
assert!(button.attributes.contains_key("label"));
assert!(button.attributes.contains_key("enabled"));
assert_eq!(button.events.len(), 1);
let text_input = &doc.root.children[2];
assert!(text_input.attributes.contains_key("value"));
assert!(text_input.attributes.contains_key("placeholder"));
let checkbox = &doc.root.children[3];
assert!(checkbox.attributes.contains_key("label"));
assert!(checkbox.attributes.contains_key("checked"));
let slider = &doc.root.children[4];
assert!(slider.attributes.contains_key("min"));
assert!(slider.attributes.contains_key("max"));
assert!(slider.attributes.contains_key("value"));
let pick_list = &doc.root.children[5];
assert!(pick_list.attributes.contains_key("options"));
assert!(pick_list.attributes.contains_key("selected"));
let toggler = &doc.root.children[6];
assert!(toggler.attributes.contains_key("label"));
assert!(toggler.attributes.contains_key("toggled"));
let image = &doc.root.children[7];
assert!(image.attributes.contains_key("src"));
assert!(image.attributes.contains_key("width"));
assert!(image.attributes.contains_key("height"));
}
#[test]
fn test_parse_interpolated_strings() {
let xml = r#"<text value="Hello {name}, you have {count} items and {if active then 'active' else 'inactive'}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Interpolated(parts)) => {
assert!(
parts.len() >= 5,
"Should have at least 5 parts, got {}",
parts.len()
);
let has_name_binding = parts.iter().any(|p| {
matches!(p, InterpolatedPart::Binding(expr) if matches!(&expr.expr, dampen_core::expr::Expr::FieldAccess(f) if f.path == vec!["name"]))
});
let has_count_binding = parts.iter().any(|p| {
matches!(p, InterpolatedPart::Binding(expr) if matches!(&expr.expr, dampen_core::expr::Expr::FieldAccess(f) if f.path == vec!["count"]))
});
let has_conditional = parts.iter().any(|p| {
matches!(p, InterpolatedPart::Binding(expr) if matches!(&expr.expr, dampen_core::expr::Expr::Conditional(_)))
});
assert!(has_name_binding, "Should contain name binding");
assert!(has_count_binding, "Should contain count binding");
assert!(has_conditional, "Should contain conditional expression");
}
_ => panic!("Expected interpolated attribute"),
}
}
#[test]
fn test_parse_nested_binding_paths() {
let xml = r#"<text value="{user.profile.name}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::FieldAccess(field) = &expr.expr {
assert_eq!(field.path, vec!["user", "profile", "name"]);
} else {
panic!("Expected field access with nested path");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_method_calls() {
let xml = r#"<text value="{items.len()}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::MethodCall(method) = &expr.expr {
assert_eq!(method.method, "len");
if let dampen_core::expr::Expr::FieldAccess(field) = &*method.receiver {
assert_eq!(field.path, vec!["items"]);
} else {
panic!("Expected field access as receiver");
}
} else {
panic!("Expected method call");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_binary_operations() {
let xml = r#"<text value="{count > 0}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::BinaryOp(binop) = &expr.expr {
assert_eq!(binop.op, dampen_core::expr::BinaryOp::Gt);
if let dampen_core::expr::Expr::FieldAccess(field) = &*binop.left {
assert_eq!(field.path, vec!["count"]);
} else {
panic!("Expected field access on left");
}
if let dampen_core::expr::Expr::Literal(lit) = &*binop.right {
assert_eq!(lit, &dampen_core::expr::LiteralExpr::Integer(0));
} else {
panic!("Expected literal on right");
}
} else {
panic!("Expected binary operation");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_conditional_expressions() {
let xml = r#"<text value="{if condition then 'yes' else 'no'}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::Conditional(cond) = &expr.expr {
if let dampen_core::expr::Expr::FieldAccess(field) = &*cond.condition {
assert_eq!(field.path, vec!["condition"]);
} else {
panic!("Expected field access in condition");
}
if let dampen_core::expr::Expr::Literal(lit) = &*cond.then_branch {
assert_eq!(
lit,
&dampen_core::expr::LiteralExpr::String("yes".to_string())
);
} else {
panic!("Expected literal in then branch");
}
if let dampen_core::expr::Expr::Literal(lit) = &*cond.else_branch {
assert_eq!(
lit,
&dampen_core::expr::LiteralExpr::String("no".to_string())
);
} else {
panic!("Expected literal in else branch");
}
} else {
panic!("Expected conditional expression");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_unary_operations() {
let xml = r#"<text value="{!is_valid}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::UnaryOp(unop) = &expr.expr {
assert_eq!(unop.op, dampen_core::expr::UnaryOp::Not);
if let dampen_core::expr::Expr::FieldAccess(field) = &*unop.operand {
assert_eq!(field.path, vec!["is_valid"]);
} else {
panic!("Expected field access as operand");
}
} else {
panic!("Expected unary operation");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_complex_expressions() {
let xml = r#"<text value="{items.len() > 0}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(_)) => {
assert!(true);
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_all_event_types() {
let xml = r#"<column>
<button on_click="click" />
<button on_press="press" />
<button on_release="release" />
<checkbox on_change="change" />
<text_input on_input="input" />
<text_input on_change="change" />
<text_input on_submit="submit" />
<pick_list options="Option1,Option2" on_select="select" />
<toggler on_toggle="toggle" />
<slider on_change="change" />
<scrollable on_scroll="scroll" />
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
let all_events: Vec<_> = doc
.root
.children
.iter()
.flat_map(|c| &c.events)
.map(|e| e.event.clone())
.collect();
assert!(all_events.contains(&EventKind::Click));
assert!(all_events.contains(&EventKind::Press));
assert!(all_events.contains(&EventKind::Release));
assert!(all_events.contains(&EventKind::Input));
assert!(all_events.contains(&EventKind::Change));
assert!(all_events.contains(&EventKind::Submit));
assert!(all_events.contains(&EventKind::Select));
assert!(all_events.contains(&EventKind::Toggle));
assert!(all_events.contains(&EventKind::Scroll));
}
#[test]
fn test_parse_id_attribute() {
let xml = r#"<column id="main_container">
<text id="header" value="Hello" />
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert_eq!(doc.root.id, Some("main_container".to_string()));
assert_eq!(doc.root.children[0].id, Some("header".to_string()));
}
#[test]
fn test_parse_mixed_attributes_and_events() {
let xml = r#"<button
id="submit_btn"
label="Submit"
enabled="{is_valid}"
on_click="submit_form"
on_press="highlight"
style="primary"
/>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert_eq!(doc.root.id, Some("submit_btn".to_string()));
assert!(matches!(
doc.root.attributes.get("label"),
Some(AttributeValue::Static(_))
));
assert!(matches!(
doc.root.attributes.get("style"),
Some(AttributeValue::Static(_))
));
assert!(matches!(
doc.root.attributes.get("enabled"),
Some(AttributeValue::Binding(_))
));
assert_eq!(doc.root.events.len(), 2);
}
#[test]
fn test_parse_empty_attribute_values() {
let xml = r#"<text value="" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Static(v)) => assert_eq!(v, ""),
_ => panic!("Expected empty static value"),
}
}
#[test]
fn test_parse_whitespace_in_attributes() {
let xml = r#"<text value=" spaces " />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Static(v)) => assert_eq!(v, " spaces "),
_ => panic!("Expected value with whitespace"),
}
}
#[test]
fn test_parse_binding_with_arithmetic() {
let xml = r#"<text value="{count + 1}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::BinaryOp(binop) = &expr.expr {
assert_eq!(binop.op, dampen_core::expr::BinaryOp::Add);
} else {
panic!("Expected binary operation");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_binding_with_comparison() {
let xml = r#"<button enabled="{count >= 5}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("enabled") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::BinaryOp(binop) = &expr.expr {
assert_eq!(binop.op, dampen_core::expr::BinaryOp::Ge);
} else {
panic!("Expected binary operation");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_binding_with_logical_ops() {
let xml = r#"<button enabled="{is_valid == true}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("enabled") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::BinaryOp(binop) = &expr.expr {
assert_eq!(binop.op, dampen_core::expr::BinaryOp::Eq);
} else {
assert!(true);
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_binding_with_negation() {
let xml = r#"<text value="{-offset}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::UnaryOp(unop) = &expr.expr {
assert_eq!(unop.op, dampen_core::expr::UnaryOp::Neg);
} else {
panic!("Expected unary operation");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_binding_with_method_and_args() {
let xml = r#"<text value="{items.contains('test')}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::MethodCall(method) = &expr.expr {
assert_eq!(method.method, "contains");
assert_eq!(method.args.len(), 1);
if let dampen_core::expr::Expr::Literal(lit) = &method.args[0] {
assert_eq!(
lit,
&dampen_core::expr::LiteralExpr::String("test".to_string())
);
} else {
panic!("Expected literal argument");
}
} else {
panic!("Expected method call");
}
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_binding_with_nested_method() {
let xml = r#"<text value="{items.first().to_uppercase()}" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.attributes.get("value") {
Some(AttributeValue::Binding(_)) => {
assert!(true);
}
_ => panic!("Expected binding"),
}
}
#[test]
fn test_parse_all_literal_types() {
let xml = r#"<column>
<text value="string literal" />
<text value="{42}" />
<text value="{3.14}" />
<text value="{true}" />
<text value="{false}" />
</column>"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
match doc.root.children[0].attributes.get("value") {
Some(AttributeValue::Static(v)) => assert_eq!(v, "string literal"),
_ => panic!("Expected string literal"),
}
match doc.root.children[1].attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::Literal(lit) = &expr.expr {
assert_eq!(lit, &dampen_core::expr::LiteralExpr::Integer(42));
} else {
panic!("Expected integer literal");
}
}
_ => panic!("Expected binding"),
}
match doc.root.children[2].attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::Literal(lit) = &expr.expr {
assert_eq!(lit, &dampen_core::expr::LiteralExpr::Float(3.14));
} else {
panic!("Expected float literal");
}
}
_ => panic!("Expected binding"),
}
match doc.root.children[3].attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::Literal(lit) = &expr.expr {
assert_eq!(lit, &dampen_core::expr::LiteralExpr::Bool(true));
} else {
panic!("Expected bool literal");
}
}
_ => panic!("Expected binding"),
}
match doc.root.children[4].attributes.get("value") {
Some(AttributeValue::Binding(expr)) => {
if let dampen_core::expr::Expr::Literal(lit) = &expr.expr {
assert_eq!(lit, &dampen_core::expr::LiteralExpr::Bool(false));
} else {
panic!("Expected bool literal");
}
}
_ => panic!("Expected binding"),
}
}
use dampen_core::ir::layout::{Alignment, Direction, Justification, Length};
use dampen_core::ir::style::Background;
#[test]
fn test_parse_flex_sizing_attributes() {
let xml = "<column><row width=\"fill\" height=\"shrink\" /><container width=\"fill_portion(3)\" /><container width=\"50%\" height=\"75%\" /></column>";
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
let row = &doc.root.children[0];
assert!(row.layout.is_some());
let layout = row.layout.as_ref().unwrap();
assert_eq!(layout.width, Some(Length::Fill));
assert_eq!(layout.height, Some(Length::Shrink));
let container1 = &doc.root.children[1];
assert!(container1.layout.is_some());
let layout1 = container1.layout.as_ref().unwrap();
assert_eq!(layout1.width, Some(Length::FillPortion(3)));
let container2 = &doc.root.children[2];
assert!(container2.layout.is_some());
let layout2 = container2.layout.as_ref().unwrap();
assert_eq!(layout2.width, Some(Length::Percentage(50.0)));
assert_eq!(layout2.height, Some(Length::Percentage(75.0)));
}
#[test]
fn test_parse_style_attributes() {
let xml = "<container background=\"#ff0000\" border_width=\"2\" border_color=\"#000000\" border_radius=\"8\" opacity=\"0.5\" />";
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.style.is_some());
let style = doc.root.style.as_ref().unwrap();
assert!(style.background.is_some());
if let Background::Color(color) = style.background.as_ref().unwrap() {
assert_eq!(color.r, 1.0);
assert_eq!(color.g, 0.0);
assert_eq!(color.b, 0.0);
assert_eq!(color.a, 1.0);
} else {
panic!("Expected color background");
}
assert!(style.border.is_some());
let border = style.border.as_ref().unwrap();
assert_eq!(border.width, 2.0);
assert_eq!(border.color.r, 0.0);
assert_eq!(border.radius.top_left, 8.0);
assert_eq!(style.opacity, Some(0.5));
}
#[test]
fn test_parse_combined_layout_and_style() {
let xml = "<column padding=\"40\" spacing=\"20\" width=\"fill\" background=\"#ffffff\"><text value=\"Test\" /></column>";
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
assert_eq!(layout.padding.as_ref().unwrap().top, 40.0);
assert_eq!(layout.spacing, Some(20.0));
assert_eq!(layout.width, Some(Length::Fill));
assert!(doc.root.style.is_some());
let style = doc.root.style.as_ref().unwrap();
assert!(style.background.is_some());
}
#[test]
fn test_parse_min_max_constraints() {
let xml = "<container width=\"fill\" min_width=\"300\" max_width=\"600\" />";
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
assert_eq!(layout.width, Some(Length::Fill));
assert_eq!(layout.min_width, Some(300.0));
assert_eq!(layout.max_width, Some(600.0));
}
#[test]
fn test_parse_alignment_attributes() {
let xml = "<row align_items=\"center\" justify_content=\"space_between\" align_self=\"end\" />";
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
assert_eq!(layout.align_items, Some(Alignment::Center));
assert_eq!(layout.justify_content, Some(Justification::SpaceBetween));
assert_eq!(layout.align_self, Some(Alignment::End));
}
#[test]
fn test_parse_align_shorthand() {
let xml = "<column align=\"center\" />";
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
assert_eq!(layout.align_items, Some(Alignment::Center));
assert_eq!(layout.justify_content, Some(Justification::Center));
}
#[test]
fn test_parse_direction() {
let xml = "<row direction=\"horizontal_reverse\" />";
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
assert_eq!(layout.direction, Some(Direction::HorizontalReverse));
}
#[test]
fn test_parse_position_attributes() {
let xml = r#"<container position="absolute" top="10" right="20" bottom="30" left="40" z_index="100" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
use dampen_core::ir::layout::Position;
assert_eq!(layout.position, Some(Position::Absolute));
assert_eq!(layout.top, Some(10.0));
assert_eq!(layout.right, Some(20.0));
assert_eq!(layout.bottom, Some(30.0));
assert_eq!(layout.left, Some(40.0));
assert_eq!(layout.z_index, Some(100));
}
#[test]
fn test_parse_position_relative() {
let xml = r#"<container position="relative" top="5" left="5" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
use dampen_core::ir::layout::Position;
assert_eq!(layout.position, Some(Position::Relative));
assert_eq!(layout.top, Some(5.0));
assert_eq!(layout.left, Some(5.0));
assert_eq!(layout.right, None);
assert_eq!(layout.bottom, None);
assert_eq!(layout.z_index, None);
}
#[test]
fn test_parse_position_partial_offsets() {
let xml = r#"<container position="absolute" top="10" right="20" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
use dampen_core::ir::layout::Position;
assert_eq!(layout.position, Some(Position::Absolute));
assert_eq!(layout.top, Some(10.0));
assert_eq!(layout.right, Some(20.0));
assert_eq!(layout.bottom, None);
assert_eq!(layout.left, None);
}
#[test]
fn test_parse_position_with_alignment() {
let xml = r#"<container position="absolute" top="10" align_items="center" />"#;
let result = parse(xml);
assert!(result.is_ok());
let doc = result.unwrap();
assert!(doc.root.layout.is_some());
let layout = doc.root.layout.as_ref().unwrap();
use dampen_core::ir::layout::Position;
assert_eq!(layout.position, Some(Position::Absolute));
assert_eq!(layout.top, Some(10.0));
assert_eq!(layout.align_items, Some(Alignment::Center));
}
#[test]
fn test_parse_invalid_position() {
let xml = r#"<container position="fixed" />"#;
let result = parse(xml);
assert!(result.is_err());
}
#[test]
fn test_parse_position_without_offsets() {
let xml = r#"<container position="absolute" />"#;
let result = parse(xml);
assert!(result.is_err());
}
#[test]
fn test_parse_shared_simple_field() {
use dampen_core::expr::{Expr, SharedFieldAccessExpr, tokenize_binding_expr};
let expr = "shared.theme";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::SharedFieldAccess(SharedFieldAccessExpr { path }) => {
assert_eq!(path, vec!["theme"]);
}
_ => panic!("Expected SharedFieldAccess, got {:?}", result.expr),
}
}
#[test]
fn test_parse_shared_nested_field() {
use dampen_core::expr::{Expr, SharedFieldAccessExpr, tokenize_binding_expr};
let expr = "shared.user.preferences.theme";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::SharedFieldAccess(SharedFieldAccessExpr { path }) => {
assert_eq!(path, vec!["user", "preferences", "theme"]);
}
_ => panic!("Expected SharedFieldAccess, got {:?}", result.expr),
}
}
#[test]
fn test_parse_shared_field_does_not_break_regular_fields() {
use dampen_core::expr::{Expr, FieldAccessExpr, tokenize_binding_expr};
let expr = "theme";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::FieldAccess(FieldAccessExpr { path }) => {
assert_eq!(path, vec!["theme"]);
}
_ => panic!("Expected FieldAccess, got {:?}", result.expr),
}
let expr2 = "model.user.name";
let result2 = tokenize_binding_expr(expr2, 0, 1, 1).unwrap();
match result2.expr {
Expr::FieldAccess(FieldAccessExpr { path }) => {
assert_eq!(path, vec!["model", "user", "name"]);
}
_ => panic!("Expected FieldAccess, got {:?}", result2.expr),
}
}
#[test]
fn test_parse_shared_field_alone_is_field_access() {
use dampen_core::expr::{Expr, FieldAccessExpr, tokenize_binding_expr};
let expr = "shared";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::FieldAccess(FieldAccessExpr { path }) => {
assert_eq!(path, vec!["shared"]);
}
_ => panic!(
"Expected FieldAccess for bare 'shared', got {:?}",
result.expr
),
}
}
#[test]
fn test_parse_shared_field_in_comparison() {
use dampen_core::expr::{BinaryOp, Expr, tokenize_binding_expr};
let expr = "shared.theme == 'dark'";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::BinaryOp(bo) => {
assert_eq!(bo.op, BinaryOp::Eq);
match *bo.left {
Expr::SharedFieldAccess(sa) => {
assert_eq!(sa.path, vec!["theme"]);
}
_ => panic!("Expected SharedFieldAccess on left"),
}
}
_ => panic!("Expected BinaryOp, got {:?}", result.expr),
}
}
#[test]
fn test_parse_shared_field_in_conditional() {
use dampen_core::expr::{Expr, tokenize_binding_expr};
let expr = "if shared.is_admin then 'Admin' else 'User'";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::Conditional(cond) => match *cond.condition {
Expr::SharedFieldAccess(sa) => {
assert_eq!(sa.path, vec!["is_admin"]);
}
_ => panic!("Expected SharedFieldAccess in condition"),
},
_ => panic!("Expected Conditional, got {:?}", result.expr),
}
}
#[test]
fn test_parse_mixed_model_and_shared_bindings() {
use dampen_core::expr::{BinaryOp, Expr, tokenize_binding_expr};
let expr = "count + shared.offset";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::BinaryOp(bo) => {
assert_eq!(bo.op, BinaryOp::Add);
match *bo.left {
Expr::FieldAccess(fa) => {
assert_eq!(fa.path, vec!["count"]);
}
_ => panic!("Expected FieldAccess on left"),
}
match *bo.right {
Expr::SharedFieldAccess(sa) => {
assert_eq!(sa.path, vec!["offset"]);
}
_ => panic!("Expected SharedFieldAccess on right"),
}
}
_ => panic!("Expected BinaryOp, got {:?}", result.expr),
}
}
#[test]
fn test_parse_shared_method_call() {
use dampen_core::expr::{Expr, tokenize_binding_expr};
let expr = "shared.items.len()";
let result = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
match result.expr {
Expr::MethodCall(mc) => {
assert_eq!(mc.method, "len");
match *mc.receiver {
Expr::SharedFieldAccess(sa) => {
assert_eq!(sa.path, vec!["items"]);
}
_ => panic!("Expected SharedFieldAccess as receiver"),
}
}
_ => panic!("Expected MethodCall, got {:?}", result.expr),
}
}
#[test]
fn test_parse_shared_uses_shared_method() {
use dampen_core::expr::tokenize_binding_expr;
let expr1 = "shared.theme";
let result1 = tokenize_binding_expr(expr1, 0, 1, 1).unwrap();
assert!(result1.expr.uses_shared());
assert!(!result1.expr.uses_model());
let expr2 = "count";
let result2 = tokenize_binding_expr(expr2, 0, 1, 1).unwrap();
assert!(!result2.expr.uses_shared());
assert!(result2.expr.uses_model());
let expr3 = "count + shared.offset";
let result3 = tokenize_binding_expr(expr3, 0, 1, 1).unwrap();
assert!(result3.expr.uses_shared());
assert!(result3.expr.uses_model());
}
#[test]
fn test_evaluate_shared_simple_field() {
use dampen_core::binding::{BindingValue, UiBindable};
use dampen_core::expr::{evaluate_expr_with_shared, tokenize_binding_expr};
struct SharedState {
theme: String,
}
impl UiBindable for SharedState {
fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
match path {
["theme"] => Some(BindingValue::String(self.theme.clone())),
_ => None,
}
}
fn available_fields() -> Vec<String> {
vec!["theme".to_string()]
}
}
struct EmptyModel;
impl UiBindable for EmptyModel {
fn get_field(&self, _path: &[&str]) -> Option<BindingValue> {
None
}
fn available_fields() -> Vec<String> {
vec![]
}
}
let shared = SharedState {
theme: "dark".to_string(),
};
let model = EmptyModel;
let expr = "shared.theme";
let binding = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
let result = evaluate_expr_with_shared(&binding.expr, &model, Some(&shared)).unwrap();
assert_eq!(result, BindingValue::String("dark".to_string()));
}
#[test]
fn test_evaluate_shared_graceful_degradation_no_context() {
use dampen_core::binding::{BindingValue, UiBindable};
use dampen_core::expr::{evaluate_expr_with_shared, tokenize_binding_expr};
struct EmptyModel;
impl UiBindable for EmptyModel {
fn get_field(&self, _path: &[&str]) -> Option<BindingValue> {
None
}
fn available_fields() -> Vec<String> {
vec![]
}
}
let model = EmptyModel;
let expr = "shared.theme";
let binding = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
let result = evaluate_expr_with_shared(&binding.expr, &model, None).unwrap();
assert_eq!(result, BindingValue::String(String::new()));
}
#[test]
fn test_evaluate_mixed_model_and_shared() {
use dampen_core::binding::{BindingValue, UiBindable};
use dampen_core::expr::{evaluate_expr_with_shared, tokenize_binding_expr};
struct Model {
count: i32,
}
impl UiBindable for Model {
fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
match path {
["count"] => Some(BindingValue::Integer(self.count as i64)),
_ => None,
}
}
fn available_fields() -> Vec<String> {
vec!["count".to_string()]
}
}
struct SharedState {
offset: i32,
}
impl UiBindable for SharedState {
fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
match path {
["offset"] => Some(BindingValue::Integer(self.offset as i64)),
_ => None,
}
}
fn available_fields() -> Vec<String> {
vec!["offset".to_string()]
}
}
let model = Model { count: 5 };
let shared = SharedState { offset: 10 };
let expr = "count + shared.offset";
let binding = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
let result = evaluate_expr_with_shared(&binding.expr, &model, Some(&shared)).unwrap();
assert_eq!(result, BindingValue::Integer(15));
}
#[test]
fn test_evaluate_shared_in_conditional() {
use dampen_core::binding::{BindingValue, UiBindable};
use dampen_core::expr::{evaluate_expr_with_shared, tokenize_binding_expr};
struct SharedState {
is_admin: bool,
}
impl UiBindable for SharedState {
fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
match path {
["is_admin"] => Some(BindingValue::Bool(self.is_admin)),
_ => None,
}
}
fn available_fields() -> Vec<String> {
vec!["is_admin".to_string()]
}
}
struct EmptyModel;
impl UiBindable for EmptyModel {
fn get_field(&self, _path: &[&str]) -> Option<BindingValue> {
None
}
fn available_fields() -> Vec<String> {
vec![]
}
}
let model = EmptyModel;
let shared_admin = SharedState { is_admin: true };
let expr = "if shared.is_admin then 'Admin' else 'User'";
let binding = tokenize_binding_expr(expr, 0, 1, 1).unwrap();
let result = evaluate_expr_with_shared(&binding.expr, &model, Some(&shared_admin)).unwrap();
assert_eq!(result, BindingValue::String("Admin".to_string()));
let shared_user = SharedState { is_admin: false };
let result = evaluate_expr_with_shared(&binding.expr, &model, Some(&shared_user)).unwrap();
assert_eq!(result, BindingValue::String("User".to_string()));
}