use std::collections::HashMap;
use webots_proto_template::{
TemplateContext, TemplateError, TemplateEvaluator, TemplateField, TemplateFieldBinding,
TemplateWebotsVersion,
};
fn to_bindings(fields: &HashMap<String, TemplateField>) -> HashMap<String, TemplateFieldBinding> {
let mut bindings = HashMap::with_capacity(fields.len());
for (field_name, field_value) in fields {
bindings.insert(
field_name.clone(),
TemplateFieldBinding::new(field_value.clone(), field_value.clone()),
);
}
bindings
}
#[test]
fn test_simple_template() {
let template = "Thing { info \"Hello %<=\"World\">%!\" }";
let fields = HashMap::new();
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(result, "Thing { info \"Hello World!\" }");
}
#[test]
fn test_field_access() {
let template = "Thing { info \"Value: %<= fields.foo.value >%\" }";
let mut fields = HashMap::new();
fields.insert(
"foo".to_string(),
TemplateField::SFString("bar".to_string()),
);
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(result, "Thing { info \"Value: bar\" }");
}
#[test]
fn test_complex_logic() {
let template = "Thing { info \"%< if (fields.check.value) { >%Yes%< } else { >%No%< } >%\" }";
let mut fields = HashMap::new();
fields.insert("check".to_string(), TemplateField::SFBool(true));
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(result, "Thing { info \"Yes\" }");
}
#[test]
fn test_template_parser_strings() {
let template = "Thing { info \"Start %< var s = \"%> is not end\"; >% End\" }";
let fields = HashMap::new();
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(result, "Thing { info \"Start End\" }");
}
#[test]
fn test_template_literal_expression() {
let template = "Thing { info \"%<= `prefix ${fields.foo.value}` >%\" }";
let mut fields = HashMap::new();
fields.insert(
"foo".to_string(),
TemplateField::SFString("bar".to_string()),
);
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(result, "Thing { info \"prefix bar\" }");
}
#[test]
fn test_math_deterministic() {
let template =
"Thing { info [ \"Random: %<= typeof Math.random >%\", \"Sin: %<= Math.sin(0) >%\" ] }";
let fields = HashMap::new();
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(
result,
"Thing { info [ \"Random: undefined\", \"Sin: 0\" ] }"
);
}
#[test]
fn test_procedural_geometry() {
let template = r#"
Group {
children [
%< for (var i = 0; i < fields.count.value; i++) { >%
Transform {
translation %<= i * 2 >% 0 0
children [
Box { size 1 1 1 }
]
}
%< } >%
]
}
"#;
let mut fields = HashMap::new();
fields.insert("count".to_string(), TemplateField::SFInt32(2));
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert!(result.contains("Group {"));
assert!(result.contains("Transform {"));
assert!(result.contains("translation 0 0 0")); assert!(result.contains("translation 2 0 0")); assert!(!result.contains("translation 4 0 0")); }
#[test]
fn test_fields_readonly() {
let template = "Thing { info %< fields.foo.value = 'mutated'; >% \"%<= fields.foo.value >%\" }";
let mut fields = HashMap::new();
fields.insert(
"foo".to_string(),
TemplateField::SFString("original".to_string()),
);
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert!(result.contains("original"));
}
#[test]
fn test_invalid_vrml_generated() {
let template = "Invalid { %<= fields.foo.value >%";
let mut fields = HashMap::new();
fields.insert("foo".to_string(), TemplateField::SFString("".to_string()));
let result =
TemplateEvaluator::new().evaluate_with_environment(template, &to_bindings(&fields));
assert!(result.is_err());
match result.unwrap_err() {
TemplateError::ValidationError(message) => {
assert!(message.contains("Syntax"));
}
other => panic!("Expected ValidationError, got {:?}", other),
}
}
#[test]
fn test_vector_js_representation() {
let template = "Vec { x %<= fields.v.value.x >% y %<= fields.v.value.y >% }";
let mut fields = HashMap::new();
fields.insert("v".to_string(), TemplateField::SFVec2f(1.0, 2.0));
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert!(result.contains("x 1"));
assert!(result.contains("y 2"));
}
#[test]
fn test_template_non_ascii_text() {
let template = "Thing { info \"Olá %<= fields.word.value >% 🌍\" }";
let mut fields = HashMap::new();
fields.insert(
"word".to_string(),
TemplateField::SFString("mundo".to_string()),
);
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(result, "Thing { info \"Olá mundo 🌍\" }");
}
#[test]
fn test_template_literal_interpolation_with_marker() {
let template = "Thing { info \"%<= `value ${\"inside >% marker\"}` >%\" }";
let fields = HashMap::new();
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert_eq!(result, "Thing { info \"value inside >% marker\" }");
}
#[test]
fn test_vector_index_access() {
let template = "Vec { x %<= fields.v.value[0] >% y %<= fields.v.value[1] >% }";
let mut fields = HashMap::new();
fields.insert("v".to_string(), TemplateField::SFVec2f(1.0, 2.0));
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert!(result.contains("x 1"));
assert!(result.contains("y 2"));
}
#[test]
fn test_js_runtime_error() {
let template = "Start %< var x = nonexistent; >% End";
let fields = HashMap::new();
let result =
TemplateEvaluator::new().evaluate_with_environment(template, &to_bindings(&fields));
assert!(result.is_err());
let err = result.unwrap_err();
println!("JS Error: {:?}", err);
}
#[test]
fn test_output_buffer_override_prevented() {
let template = "Thing { info \"safe\" %< __webots_output_buffer.push('hacked'); >% }";
let fields = HashMap::new();
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert!(!result.contains("hacked"));
}
#[test]
fn test_output_writer_override_prevented() {
let template = "Thing { info \"%< __webots_write = function(_) {}; >%safe\" }";
let fields = HashMap::new();
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &to_bindings(&fields))
.unwrap();
assert!(result.contains("safe"));
}
#[test]
fn test_field_binding_exposes_default_value() {
let template = "Thing { info \"%<= fields.count.value >%|%<= fields.count.defaultValue >%\" }";
let mut fields = HashMap::new();
fields.insert(
"count".to_string(),
TemplateFieldBinding::new(TemplateField::SFInt32(42), TemplateField::SFInt32(7)),
);
let result = TemplateEvaluator::new()
.evaluate_with_environment(template, &fields)
.unwrap();
assert_eq!(result, "Thing { info \"42|7\" }");
}
#[test]
fn test_context_is_available() {
let template = "Thing { info \"%<= context.os >%|%<= context.webots_version.major >%\" }";
let context = TemplateContext::default()
.with_os("linux")
.with_webots_version(TemplateWebotsVersion::new(
"R2025a".to_string(),
"0".to_string(),
));
let result = TemplateEvaluator::with_context(context)
.evaluate_with_environment(template, &HashMap::new())
.unwrap();
assert_eq!(result, "Thing { info \"linux|R2025a\" }");
}