use velox_sfc::{
RenderMode, compile_template_to_rs, compile_template_to_rs_full,
compile_template_to_rs_full_with_mode,
};
const SCRIPT_WITH_GETTERS: &str = r#"
pub struct State {
pub count: i32,
}
impl State {
pub fn new() -> Self {
Self { count: 0 }
}
pub fn draft(&self) -> String {
String::from("Buy milk")
}
pub fn input_placeholder(&self) -> String {
String::from("What needs to be done?")
}
}
"#;
const SCRIPT_WITH_GETTERS_AND_HANDLER: &str = r#"
import TodoItem from './components/TodoItem.vx';
pub struct State {
pub count: i32,
}
impl State {
pub fn new() -> Self {
Self { count: 0 }
}
pub fn draft(&self) -> String {
String::from("Buy milk")
}
pub fn input_placeholder(&self) -> String {
String::from("What needs to be done?")
}
pub fn active(&self) -> String {
String::from("active")
}
pub fn completed(&self) -> String {
String::from("true")
}
pub fn on_input(&self, payload: &str) {
let _ = payload;
}
/// Zero-argument, but nothing comes back, so there is no text to render.
pub fn reset(&self) {}
/// Zero-argument, but a collection has no `Display` form.
pub fn items(&self) -> Vec<String> {
Vec::new()
}
}
"#;
fn make_resolve_body(rs: &str) -> &str {
rs.split_once("pub fn make_resolve")
.expect("make_resolve should be generated")
.1
}
#[test]
fn codegen_v_for_dot_notation() {
let rs = compile_template_to_rs(
r#"<div v-for="item in items"><p>{{ item.name }}</p></div>"#,
"App",
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(rs.contains("__for_count"));
assert!(rs.contains("for __idx in 0..__for_count"));
assert!(rs.contains("items[{}].name"));
}
#[test]
fn codegen_v_for_with_index() {
let rs = compile_template_to_rs(
r#"<div v-for="(item, index) in items"><p>{{ item.name }} #{{ index }}</p></div>"#,
"App",
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(rs.contains("__for_count"));
assert!(rs.contains("for index in 0..__for_count"));
assert!(rs.contains("items[{}].name"));
}
#[test]
fn codegen_v_for_with_key() {
let rs = compile_template_to_rs(
r#"<div v-for="item in items" :key="item.id"><p>{{ item.name }}</p></div>"#,
"App",
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(rs.contains("__for_count"));
assert!(!rs.contains(r#".set("key""#));
}
#[test]
fn codegen_v_for_numeric_count() {
let rs = compile_template_to_rs(
r#"<div v-for="i in 5"><span>{{ i }}</span></div>"#,
"App",
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(rs.contains("__for_count"));
}
#[test]
fn codegen_div_with_text() {
let rs = compile_template_to_rs("<div>hi</div>", "App", None).unwrap();
assert!(rs.contains(r#"use velox_dom::*"#));
assert!(rs.contains(r#"h("div""#));
assert!(rs.contains(r#"text("hi")"#));
}
#[test]
fn codegen_interpolation() {
let rs = compile_template_to_rs("<p>Hello {{name}}</p>", "App", None).unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(rs.contains(r#"h("p""#));
assert!(rs.contains(r#"text("Hello")"#) || rs.contains(r#"text("Hello ")"#));
assert!(rs.contains(r#"resolve("name")"#) || rs.contains(r#"resolve("name")"#));
}
#[test]
fn codegen_attrs() {
let rs = compile_template_to_rs(
r#"<input class="x" :value="count" @input="onInput"/>"#,
"App",
None,
)
.unwrap();
assert!(rs.contains(r#".set("class", "x")"#));
assert!(rs.contains(r#".set("value", &resolve("count"))"#));
assert!(rs.contains(r#".set("on:input", "onInput")"#));
}
#[test]
fn component_bound_attrs_are_registered_as_resolver_keys() {
let rs = compile_template_to_rs_full(
r#"<div data-velox-component="TodoInput" :value="draft" :placeholder="input_placeholder"></div>"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(
rs.contains(r#"resolve("draft")"#),
"`:value` must be read through the resolver:\n{rs}"
);
assert!(
rs.contains(r#"resolve("input_placeholder")"#),
"`:placeholder` must be read through the resolver:\n{rs}"
);
let resolve = make_resolve_body(&rs);
assert!(
resolve.contains(r#""draft" => state.draft().to_string()"#),
"bound `:value=\"draft\"` must be a resolver key:\n{resolve}"
);
assert!(
resolve.contains(r#""input_placeholder" => state.input_placeholder().to_string()"#),
"bound `:placeholder=\"input_placeholder\"` must be a resolver key:\n{resolve}"
);
}
#[test]
fn element_bound_attr_and_class_condition_are_registered_as_resolver_keys() {
let rs = compile_template_to_rs_full(
r#"<div><input :value="draft" /><p :class="{ done: input_placeholder }">x</p></div>"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
let resolve = make_resolve_body(&rs);
assert!(
resolve.contains(r#""draft" => state.draft().to_string()"#),
"`:value=\"draft\"` must be a resolver key:\n{resolve}"
);
assert!(
resolve.contains(r#""input_placeholder" => state.input_placeholder().to_string()"#),
"`:class` condition must be a resolver key:\n{resolve}"
);
}
#[test]
fn bound_attr_without_state_getter_is_not_registered() {
let script = r#"
pub struct State {
pub count: std::rc::Rc<velox_core::signal::Signal<i32>>,
}
"#;
let rs = compile_template_to_rs_full(
r#"<input :value="count" />"#,
"Counter",
None,
Some(script),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(rs.contains(r#".set("value", &resolve("count"))"#));
let resolve = make_resolve_body(&rs);
assert!(
!resolve.contains(r#""count" =>"#),
"no getter means no resolver arm for a field-based binding:\n{resolve}"
);
}
#[test]
fn bare_class_binding_is_registered_as_resolver_key() {
let rs = compile_template_to_rs_full(
r#"<div><p :class="active">x</p></div>"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(
rs.contains(r#"resolve("active")"#),
"`:class=\"active\"` must be read through the resolver:\n{rs}"
);
let resolve = make_resolve_body(&rs);
assert!(
resolve.contains(r#""active" => state.active().to_string()"#),
"`:class=\"active\"` must be a resolver key:\n{resolve}"
);
}
#[test]
fn negated_class_condition_registers_the_condition_key() {
let rs = compile_template_to_rs_full(
r#"<div><p :class="{ done: !completed }">x</p></div>"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(
rs.contains(r#"resolve("completed")"#),
"`:class=\"{{ done: !completed }}\"` must look up `completed`:\n{rs}"
);
let resolve = make_resolve_body(&rs);
assert!(
resolve.contains(r#""completed" => state.completed().to_string()"#),
"the condition operand must be a resolver key:\n{resolve}"
);
assert!(
!resolve.contains(r#""!completed" =>"#),
"the `!`-prefixed text is not a resolver key:\n{resolve}"
);
}
#[test]
fn component_bound_object_class_registers_the_condition_key() {
let rs = compile_template_to_rs_full(
r#"<div data-velox-component="TodoItem" :class="{ done: completed }"></div>"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(
!rs.contains(r#"resolve("{ done: completed }")"#),
"the whole class literal must not be used as a resolver key:\n{rs}"
);
assert!(
rs.contains(r#"resolve("completed")"#),
"the class condition must be read through the resolver:\n{rs}"
);
let resolve = make_resolve_body(&rs);
assert!(
resolve.contains(r#""completed" => state.completed().to_string()"#),
"the class condition must be a resolver key:\n{resolve}"
);
}
#[test]
fn unsupported_bound_expression_is_not_registered_as_a_resolver_key() {
let rs = compile_template_to_rs_full(
r#"<input :value="draft.trim()" />"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
let resolve = make_resolve_body(&rs);
assert!(
!resolve.contains(r#""draft.trim()" =>"#),
"a non-bare expression must not be registered as a resolver key:\n{resolve}"
);
}
#[test]
fn payload_taking_method_is_not_registered_as_a_getter() {
let rs = compile_template_to_rs_full(
r#"<input :value="on_input" />"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
let resolve = make_resolve_body(&rs);
assert!(
!resolve.contains(r#""on_input" =>"#),
"a payload-taking handler must not be registered as a getter:\n{resolve}"
);
assert!(
!rs.contains("state.on_input()"),
"generated code must never call a handler as a getter:\n{rs}"
);
}
#[test]
fn zero_argument_method_without_return_type_is_not_registered_as_a_getter() {
let rs = compile_template_to_rs_full(
r#"<input :value="reset" />"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
let resolve = make_resolve_body(&rs);
assert!(
!resolve.contains(r#""reset" =>"#),
"a method with no declared return type is not a getter:\n{resolve}"
);
assert!(
!rs.contains("state.reset()"),
"generated code must never call a unit method as a getter:\n{rs}"
);
}
#[test]
fn zero_argument_method_with_unrenderable_return_type_is_not_registered_as_a_getter() {
let rs = compile_template_to_rs_full(
r#"<input :value="items" />"#,
"TodoApp",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
let resolve = make_resolve_body(&rs);
assert!(
!resolve.contains(r#""items" =>"#),
"a method returning `Vec<String>` is not a text getter:\n{resolve}"
);
assert!(
!rs.contains("state.items().to_string()"),
"generated code must never stringify an unrenderable value:\n{rs}"
);
}
#[test]
fn loop_rooted_binding_reads_the_loop_item_in_both_renderers() {
let script = r#"
pub struct State {
pub todos: std::rc::Rc<velox_core::signal::Signal<Vec<String>>>,
}
impl State {
pub fn todos(&self) -> Vec<String> {
self.todos.get()
}
}
"#;
let rs = compile_template_to_rs_full(
r#"<div v-for="(todo, idx) in todos"><p :value="todo.text" :index="idx">x</p></div>"#,
"TodoApp",
None,
Some(script),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(
rs.contains(r#"resolve(&format!("todos[{}].text", idx))"#),
"Resolve mode must read the loop field through the indexed resolver:\n{rs}"
);
assert!(
!rs.contains(r#"resolve("todo.text")"#),
"Resolve mode must not look a loop-rooted expression up verbatim:\n{rs}"
);
assert!(
rs.contains(r#".set("index", &format!("{}", idx))"#),
"the loop index is a binding the Resolve body already has:\n{rs}"
);
assert!(
rs.contains(r#"format!("{}", todo.text)"#),
"State mode must keep reading the loop item directly:\n{rs}"
);
let resolve = make_resolve_body(&rs);
assert!(
!resolve.contains(r#""todo.text" =>"#),
"a loop-variable expression must not be registered as a key:\n{resolve}"
);
assert!(
!resolve.contains(r#""idx" =>"#),
"a loop index must not be registered as a key:\n{resolve}"
);
}
#[test]
fn v_for_key_generation_is_single_interpolation_in_both_renderers() {
let rs = compile_template_to_rs_full(
r#"<div v-for="todo in todos" :key="todo.id">x</div>"#,
"TodoApp",
None,
None,
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(
rs.contains(
r#"if let velox_dom::VNode::Element { ref mut props, .. } = __node { props.attrs.insert("key".to_string(), todo.id.to_string()); } __node }"#,
),
"State mode keeps inserting the loop field:\n{rs}"
);
assert!(
rs.contains(
r#"if let velox_dom::VNode::Element { ref mut props, .. } = __node { props.attrs.insert("key".to_string(), resolve(&format!("todos[{}].id", __idx)).to_string()); } __node }"#,
),
"Resolve mode reads the loop-rooted key exactly once, through the loop:\n{rs}"
);
}
#[test]
fn render_mode_changes_diagnostics_not_generated_code() {
let template =
r#"<div v-for="(todo, idx) in todos"><TodoItem :todo="todo.text" :index="idx" /></div>"#;
let script = r#"
pub struct Todo {
pub text: String,
}
pub struct State {
pub todos: std::rc::Rc<velox_core::signal::Signal<Vec<Todo>>>,
}
impl State {
pub fn new() -> Self {
State { todos: std::rc::Rc::new(velox_core::signal::Signal::new(Vec::new())) }
}
}
"#;
let state = compile_template_to_rs_full_with_mode(
template,
"TodoApp",
None,
Some(script),
None,
RenderMode::State,
)
.unwrap();
let resolve = compile_template_to_rs_full_with_mode(
template,
"TodoApp",
None,
Some(script),
None,
RenderMode::Resolve,
)
.unwrap();
assert_eq!(
state, resolve,
"the mode must only select diagnostics, not codegen output"
);
let defaulted =
compile_template_to_rs_full(template, "TodoApp", None, Some(script), None).unwrap();
assert_eq!(
state, defaulted,
"the default entry point must compile in State mode"
);
}
#[test]
fn field_with_a_same_named_getter_still_resolves_through_the_getter() {
let script = r#"
pub struct State {
pub draft: std::rc::Rc<velox_core::signal::Signal<String>>,
}
impl State {
pub fn draft(&self) -> String {
self.draft.get().clone()
}
}
"#;
let rs = compile_template_to_rs_full(
r#"<input :value="draft" />"#,
"TodoApp",
None,
Some(script),
None,
)
.unwrap();
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
let resolve = make_resolve_body(&rs);
assert!(
resolve.contains(r#""draft" => state.draft().to_string()"#),
"a same-named getter must still register the key:\n{resolve}"
);
}
const V_FOR_KEY_MUSTACHE: &str =
r#"<ul><li v-for="item in items" :key="{{ item.id }}">{{ item.name }}</li></ul>"#;
const V_FOR_KEY_PLAIN: &str =
r#"<ul><li v-for="item in items" :key="item.id">{{ item.name }}</li></ul>"#;
const V_FOR_NO_KEY: &str = r#"<ul><li v-for="item in items">{{ item.name }}</li></ul>"#;
const V_FOR_KEY_VALUELESS: &str = r#"<ul><li v-for="item in items" :key>{{ item.name }}</li></ul>"#;
fn key_statements(rs: &str) -> Vec<&str> {
rs.lines()
.filter(|line| line.contains(r#"props.attrs.insert("key""#))
.collect()
}
fn state_renderer(rs: &str) -> &str {
let start = rs
.find("pub fn render_with_state")
.expect("the State-mode renderer is always generated");
let rest = &rs[start..];
let end = rest
.find("pub fn make_resolve")
.expect("make_resolve is always generated");
&rest[..end]
}
fn resolve_renderer(rs: &str) -> &str {
let start = rs
.find("pub fn render_with<F>")
.expect("the Resolve-mode renderer is always generated");
let rest = &rs[start..];
let end = rest
.find("pub fn render_with_state")
.expect("the State-mode renderer is always generated");
&rest[..end]
}
fn compile_in_mode(tpl: &str, mode: RenderMode) -> String {
compile_template_to_rs_full_with_mode(tpl, "App", None, None, None, mode)
.expect("template compiles")
}
fn compile_with_script(tpl: &str, script: &str, mode: RenderMode) -> String {
compile_template_to_rs_full_with_mode(tpl, "App", None, Some(script), None, mode)
.expect("template compiles")
}
#[test]
fn resolve_mode_v_for_key_interpolates_the_expression_exactly_once() {
let rs = compile_in_mode(V_FOR_KEY_MUSTACHE, RenderMode::Resolve);
println!("-- GENERATED RS --\n{}\n-- END RS --", rs);
assert!(
rs.contains(
r#"props.attrs.insert("key".to_string(), resolve(&format!("items[{}].id", __idx)).to_string());"#
),
"the key must be read once, through the loop it is rooted in:\n{rs}"
);
assert_eq!(
resolve_renderer(&rs).matches("items[{}].id").count(),
1,
"the Resolve-mode renderer must interpolate the key exactly once:\n{rs}"
);
assert_eq!(
state_renderer(&rs).matches("item.id").count(),
1,
"the State-mode renderer must read the key exactly once:\n{rs}"
);
assert!(
!rs.contains("resolve({{"),
"`{{` must not reach the generated resolver call:\n{rs}"
);
assert!(
!rs.contains(r#"resolve({ let __v = resolve("#),
"a dotted key must not be rewritten into a block field access:\n{rs}"
);
}
#[test]
fn resolve_mode_v_for_key_spellings_generate_the_same_lookup() {
let mustache = compile_in_mode(V_FOR_KEY_MUSTACHE, RenderMode::Resolve);
let plain = compile_in_mode(V_FOR_KEY_PLAIN, RenderMode::Resolve);
let of =
|rs: &str| -> Vec<String> { key_statements(rs).into_iter().map(str::to_string).collect() };
assert_eq!(
of(&mustache),
of(&plain),
"`:key=\"{{{{ item.id }}}}\"` and `:key=\"item.id\"` are the same expression:\n{mustache}\n---\n{plain}"
);
}
#[test]
fn state_mode_v_for_key_output_matches_the_pre_fix_golden() {
for (tpl, golden) in [
(
V_FOR_KEY_MUSTACHE,
include_str!("testdata/v_for_key_mustache.state_renderer.golden"),
),
(
V_FOR_KEY_PLAIN,
include_str!("testdata/v_for_key_plain.state_renderer.golden"),
),
] {
let rs = compile_in_mode(tpl, RenderMode::State);
assert_eq!(
state_renderer(&rs),
golden,
"the State-mode renderer must be unchanged for {tpl}"
);
}
}
#[test]
fn v_for_without_a_key_output_matches_the_pre_fix_golden() {
let module_golden = include_str!("testdata/v_for_without_key.module.golden");
for mode in [RenderMode::Resolve, RenderMode::State] {
let rs = compile_in_mode(V_FOR_NO_KEY, mode);
assert_eq!(
rs, module_golden,
"a keyless `v-for` module must be unchanged in {mode:?}"
);
assert_eq!(
resolve_renderer(&rs),
include_str!("testdata/v_for_without_key.resolve_renderer.golden"),
"the keyless Resolve-mode renderer must be unchanged in {mode:?}"
);
}
}
#[test]
fn v_for_with_a_valueless_key_generates_the_keyless_module() {
for mode in [RenderMode::Resolve, RenderMode::State] {
let rs = compile_in_mode(V_FOR_KEY_VALUELESS, mode);
assert!(
key_statements(&rs).is_empty(),
"a valueless `:key` must not insert a key in {mode:?}:\n{rs}"
);
assert_eq!(
rs,
include_str!("testdata/v_for_without_key.module.golden"),
"a valueless `:key` must generate the keyless module in {mode:?}"
);
}
}
fn make_resolve_fn(rs: &str) -> &str {
let start = rs
.find("pub fn make_resolve")
.expect("make_resolve is always generated");
let rest = &rs[start..];
let end = rest
.find("pub fn make_on_event")
.expect("make_on_event is always generated");
&rest[..end]
}
fn make_resolve_arms(rs: &str) -> Vec<&str> {
let body = make_resolve_fn(rs);
let start = body
.find("match name {")
.expect("make_resolve matches on a name");
let rest = &body[start..];
let end = rest
.find(
"
}",
)
.expect("the match closes");
rest[..end]
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with("match name"))
.collect()
}
#[test]
fn a_loop_rooted_interpolation_produces_no_resolver_arm() {
for mode in [RenderMode::State, RenderMode::Resolve] {
let rs = compile_in_mode(V_FOR_NO_KEY, mode);
assert_eq!(
make_resolve_arms(&rs),
vec!["_ => String::new(),"],
"a loop-rooted interpolation is not a `State` field, so it cannot \
be an arm in {mode:?}"
);
assert!(
!rs.contains("state.item."),
"nothing may read the loop item as a state field path in {mode:?} \
(`state.items.get()` is the collection, and is expected):\n{rs}"
);
}
}
#[test]
fn a_loop_rooted_interpolation_still_renders_through_the_loop_index() {
for mode in [RenderMode::State, RenderMode::Resolve] {
let module = compile_in_mode(V_FOR_NO_KEY, mode);
let resolve_r = resolve_renderer(&module);
assert!(
resolve_r.contains(r#"text(resolve(&format!("items[{}].name", __idx)))"#),
"the Resolve body reads the loop item through the index in {mode:?}:\n{resolve_r}"
);
}
let state_module = compile_in_mode(V_FOR_NO_KEY, RenderMode::State);
let state_r = state_renderer(&state_module);
assert!(
state_r.contains("__obj.name.to_string()"),
"the State body reads the loop binding directly:\n{state_r}"
);
}
const SCRIPT_WITH_A_USER_ACCESSOR: &str = r#"
pub struct User { name: String }
impl User { pub fn name(&self) -> String { self.name.clone() } }
pub struct State { user: User }
impl State {
pub fn new() -> Self { Self { user: User { name: String::new() } } }
pub fn user(&self) -> User { self.user.clone() }
pub fn title(&self) -> String { String::new() }
}
"#;
#[test]
fn a_root_level_member_path_emits_a_chain_on_the_root_accessor() {
for mode in [RenderMode::State, RenderMode::Resolve] {
let rs = compile_with_script(
r#"<p>{{ user.name }}</p>"#,
SCRIPT_WITH_A_USER_ACCESSOR,
mode,
);
assert!(
make_resolve_fn(&rs).contains(r#""user.name" => state.user().name().to_string(),"#),
"the root is called, and so is every segment after it, in {mode:?}:\n{rs}"
);
}
}
#[test]
fn a_root_level_indexed_member_path_indexes_the_root_accessor() {
let script = r#"
pub struct Item { name: String }
impl Item { pub fn name(&self) -> String { self.name.clone() } }
pub struct State { items: Vec<Item> }
impl State {
pub fn new() -> Self { Self { items: Vec::new() } }
pub fn items(&self) -> Vec<Item> { self.items.clone() }
}
"#;
for mode in [RenderMode::State, RenderMode::Resolve] {
let rs = compile_with_script(r#"<p>{{ items[0].name }}</p>"#, script, mode);
assert!(
make_resolve_fn(&rs)
.contains(r#""items[0].name" => state.items()[0].name().to_string(),"#),
"an index is a place, not a method, and the segments around it are calls, in {mode:?}:\n{rs}"
);
}
}
#[test]
fn a_root_level_member_path_without_an_accessor_emits_no_arm() {
for mode in [RenderMode::State, RenderMode::Resolve] {
let rs = compile_in_mode(r#"<p>{{ user.name }}</p>"#, mode);
assert!(
!make_resolve_fn(&rs).contains("user.name"),
"an unanswerable member path must not be registered, so it cannot emit the \
arm `state.user.name()` that does not compile, in {mode:?}:\n{rs}"
);
}
}
#[test]
fn a_call_interpolation_key_is_left_exactly_as_it_was() {
for mode in [RenderMode::State, RenderMode::Resolve] {
let rs = compile_in_mode(r#"<p>{{ user.name() }}</p>"#, mode);
assert!(
make_resolve_fn(&rs).contains(r#""user.name()" => state.user.name()().to_string(),"#),
"a call key keeps the fallback arm it always had, in {mode:?}:\n{rs}"
);
}
}
#[test]
fn a_bare_interpolation_is_registered_with_a_getter_and_dropped_without_one() {
const GETTER: &str = r#"
impl State {
pub fn title(&self) -> String { String::new() }
}
"#;
for mode in [RenderMode::State, RenderMode::Resolve] {
let with = compile_with_script(r#"<p>{{ title }}</p>"#, GETTER, mode);
assert_eq!(
make_resolve_arms(&with),
vec![
r#""title" => state.title().to_string(),"#.to_string(),
"_ => String::new(),".to_string(),
],
"a getter-backed bare name is emitted byte-identically, in {mode:?}:\n{with}"
);
let without = compile_in_mode(r#"<p>{{ title }}</p>"#, mode);
assert_eq!(
make_resolve_arms(&without),
vec!["_ => String::new(),".to_string()],
"with no getter the arm would not compile, so the key is not \
registered, in {mode:?}:\n{without}"
);
}
}
const LOOP_ROOTED_BINDS: &str =
r#"<li v-for="todo in todos" :key="todo.id" :data-id="todo.id">{{ todo.text }}</li>"#;
const LOOP_ROOTED_COMPONENT_BINDS: &str = r#"<TodoItem v-for="(todo, index) in todos" :key="todo.id" :todo="todo.text" :index="index" />"#;
#[test]
fn resolve_mode_loop_rooted_binding_is_read_from_the_loop_item() {
let rs = compile_in_mode(LOOP_ROOTED_BINDS, RenderMode::Resolve);
let resolve_r = resolve_renderer(&rs);
assert!(
resolve_r.contains(r#"for __idx in 0..__for_count {"#),
"the Resolve loop body binds the index:\n{resolve_r}"
);
assert!(
resolve_r.contains(
r#".set("data-id", &format!("{}", resolve(&format!("todos[{}].id", __idx))))"#
),
"an element binding rooted at the loop item must read the loop item:\n{resolve_r}"
);
assert!(
resolve_r.contains(
r#"props.attrs.insert("key".to_string(), resolve(&format!("todos[{}].id", __idx)).to_string());"#
),
"the `:key` must be read from the loop item:\n{resolve_r}"
);
assert!(
!resolve_r.contains(r#"resolve("todo.id")"#),
"a loop-rooted binding must not be looked up in the resolver:\n{resolve_r}"
);
assert!(
!make_resolve_body(&rs).contains(r#""todo.id" =>"#),
"a loop-rooted binding must not be registered as a resolver key either:\n{rs}"
);
let rs = compile_in_mode(LOOP_ROOTED_COMPONENT_BINDS, RenderMode::Resolve);
let resolve_r = resolve_renderer(&rs);
assert!(
resolve_r.contains(
r#".set("todo", &format!("{}", resolve(&format!("todos[{}].text", index))))"#
),
"a component prop rooted at the loop item must read the loop item:\n{resolve_r}"
);
assert!(
resolve_r.contains(r#".set("index", &format!("{}", index))"#),
"a component prop rooted at the loop index must read the index:\n{resolve_r}"
);
assert!(
!resolve_r.contains(r#"resolve("todo.text")"#)
&& !resolve_r.contains(r#"resolve("index")"#),
"neither the item nor the index is a resolver key:\n{resolve_r}"
);
assert!(
resolve_r.contains(r#"let __for_expr = resolve("todos");"#),
"the collection lookup must stay as it is:\n{resolve_r}"
);
}
#[test]
fn resolve_mode_unreadable_loop_rooted_binding_keeps_a_reported_lookup() {
let rs = compile_in_mode(
r#"<li v-for="todo in todos" :data-id="todo.a + todo.b">x</li>"#,
RenderMode::Resolve,
);
let resolve_r = resolve_renderer(&rs);
assert!(
resolve_r.contains(r#".set("data-id", &resolve("todo.a + todo.b"))"#),
"an expression that is not a field path must stay a plain lookup:\n{resolve_r}"
);
assert!(
!resolve_r.contains("todos[{}].a + todo.b"),
"only a field path may be rewritten into an indexed read:\n{resolve_r}"
);
}
#[test]
fn resolve_mode_non_loop_binding_still_resolves_through_the_resolver() {
let rs = compile_template_to_rs_full_with_mode(
r#"<div><input :value="draft" :placeholder="input_placeholder" /><p :data-active="active">x</p></div>"#,
"App",
None,
Some(SCRIPT_WITH_GETTERS_AND_HANDLER),
None,
RenderMode::Resolve,
)
.expect("template compiles");
let resolve = make_resolve_body(&rs);
assert!(
resolve.contains(r#""draft" =>"#)
&& resolve.contains(r#""input_placeholder" =>"#)
&& resolve.contains(r#""active" =>"#),
"non-loop bindings must still be registered as resolver keys:\n{resolve}"
);
let resolve_r = resolve_renderer(&rs);
assert!(
resolve_r.contains(r#"resolve("draft")"#)
&& resolve_r.contains(r#"resolve("input_placeholder")"#)
&& resolve_r.contains(r#"resolve("active")"#),
"non-loop bindings must still be read through the resolver:\n{resolve_r}"
);
assert!(
!resolve_r.contains("todos[{}]"),
"a loop-free template must gain no loop-rooted read:\n{resolve_r}"
);
}
#[test]
fn state_mode_loop_rooted_binding_output_is_unchanged() {
let rs = compile_in_mode(LOOP_ROOTED_BINDS, RenderMode::State);
let state_r = state_renderer(&rs);
assert!(
state_r.contains(
r#"h("li", Props::new().set("data-id", &format!("{}", todo.id)), vec![text({ let __obj = &todo; __obj.text.to_string() })]); if let velox_dom::VNode::Element { ref mut props, .. } = __node { props.attrs.insert("key".to_string(), todo.id.to_string()); } __node"#
),
"State-mode output must not change:\n{state_r}"
);
assert!(
!state_r.contains(r#"resolve("todo.id")"#),
"State mode must keep reading the loop item:\n{state_r}"
);
let rs = compile_in_mode(LOOP_ROOTED_COMPONENT_BINDS, RenderMode::State);
let state_r = state_renderer(&rs);
assert!(
state_r.contains(
r#"h("TodoItem", Props::new().set("todo", &format!("{}", todo.text)).set("index", &format!("{}", index)), vec![]); if let velox_dom::VNode::Element { ref mut props, .. } = __node { props.attrs.insert("key".to_string(), todo.id.to_string()); } __node"#
),
"State-mode component output must not change:\n{state_r}"
);
}
#[test]
fn resolve_mode_class_condition_on_a_loop_item_is_a_truthiness_test() {
for condition in [r#"todo.done"#, r#"todo.is_done"#] {
let tpl = format!(r#"<li v-for="todo in todos" :class="{{ active: {condition} }}">x</li>"#);
let rs = compile_in_mode(&tpl, RenderMode::Resolve);
let resolve_r = resolve_renderer(&rs);
assert!(
resolve_r.contains(&format!(
r#"if {{ let __v = resolve(&format!("todos[{{}}].{field}", __idx)); __v == "true" || match __v.trim().parse::<f64>() {{ Ok(__n) => __n != 0.0, Err(_) => !__v.is_empty() && __v != "false" }} }} {{ __classes.push("active"); }}"#,
field = condition.trim_start_matches("todo.")
)),
"a `String` condition must be tested for truthiness, not used as one \
({condition}):\n{resolve_r}"
);
assert!(
!resolve_r.contains(&format!("if {condition} {{")),
"the raw `String` read must not sit in `if` position ({condition}):\n{resolve_r}"
);
}
}
#[test]
fn compound_class_condition_in_a_resolve_loop_is_still_broken() {
let tpl = r#"<li v-for="todo in todos" :class="{ active: todo.a == todo.b }">x</li>"#;
let rs = compile_in_mode(tpl, RenderMode::Resolve);
let resolve_r = resolve_renderer(&rs);
assert!(
resolve_r.contains(r#"if todo.a == todo.b { __classes.push("active"); }"#),
"the compound condition is still emitted raw, and it cannot compile in \
this renderer:\n{resolve_r}"
);
assert!(
resolve_r.contains("for __idx in 0..__for_count {"),
"the body binds an index, not the item the condition names:\n{resolve_r}"
);
assert!(
state_renderer(&rs).contains(r#"if todo.a == todo.b { __classes.push("active"); }"#),
"State mode reads the same expression off the loop item, and is \
unchanged:\n{}",
state_renderer(&rs)
);
}