use crate::template_ast::{AttrKind, Node, TemplateAttr};
#[test]
fn script_setup_lifecycle_hook_macros_pass_through() {
let sfc_src = r#"<template>
<div><span>{{ title }}</span></div>
</template>
<script setup>
use velox_core::ref_value;
pub struct State { pub title: String }
impl State { pub fn new() -> Self { Self { title: String::from("Lifecycle") } } }
</script>
<style>
.app { padding: 12px; }
</style>"#;
let with_hooks = sfc_src.replace(
"<script setup>\n",
"<script setup>\nvelox_core::on_mounted! { { /* setup */ } }\nvelox_core::on_updated! { { /* update */ } }\nvelox_core::on_unmounted! { { /* teardown */ } }\n",
);
let sfc = crate::parse_sfc(&with_hooks).expect("parse sfc");
let stub = crate::to_stub_rs(&sfc, "app");
for expected in [
"velox_core::on_mounted!",
"velox_core::on_updated!",
"velox_core::on_unmounted!",
] {
assert!(
stub.contains(expected),
"generated module should contain `{expected}` verbatim"
);
}
assert!(stub.contains("pub mod script_rs"));
}
#[test]
fn emit_node_text_and_interpolation() {
let t = Node::Text("hello".to_string());
let out = crate::template_codegen::emit_node(&t);
assert_eq!(out, r#"text("hello")"#);
let i = Node::Interpolation("count".to_string());
let out2 = crate::template_codegen::emit_node(&i);
assert!(out2.contains("resolve"));
}
#[test]
fn emit_props_varieties() {
let attrs: Vec<TemplateAttr> = vec![
TemplateAttr {
name: "class".into(),
value: Some("btn".into()),
kind: AttrKind::Static,
},
TemplateAttr {
name: "on:click".into(),
value: Some("increment".into()),
kind: AttrKind::On,
},
TemplateAttr {
name: "click".into(),
value: Some("inc".into()),
kind: AttrKind::On,
},
];
let out = crate::template_codegen::emit_props(&attrs);
assert!(out.contains("set(\"class\", \"btn\")") || out.contains("class"));
assert!(out.contains("on:click"));
}
#[test]
fn emit_children_simple() {
let children = vec![Node::Text("a".into()), Node::Text("b".into())];
let out = crate::template_codegen::emit_children(&children);
assert!(out.starts_with("vec!["));
assert!(out.contains("text(\"a\")"));
}
#[test]
fn rewrite_if_expr_logical_and() {
let out = crate::template_codegen::rewrite_if_expr("is_visible && is_active");
assert!(out.contains("&&"));
assert!(out.contains("resolve(\"is_visible\")"));
assert!(out.contains("resolve(\"is_active\")"));
}
#[test]
fn rewrite_if_expr_logical_or() {
let out = crate::template_codegen::rewrite_if_expr("is_visible || is_active");
assert!(out.contains("||"));
assert!(out.contains("resolve(\"is_visible\")"));
assert!(out.contains("resolve(\"is_active\")"));
}
#[test]
fn rewrite_if_expr_negation() {
let out = crate::template_codegen::rewrite_if_expr("!is_hidden");
assert!(out.starts_with("!("));
assert!(out.contains("resolve(\"is_hidden\")"));
}
#[test]
fn rewrite_if_expr_negation_true() {
let out = crate::template_codegen::rewrite_if_expr("!true");
assert!(out.contains("!(true)") || out.contains("!true"));
}
#[test]
fn rewrite_if_expr_combined_logic() {
let out = crate::template_codegen::rewrite_if_expr("is_visible && !is_hidden");
assert!(out.contains("&&"));
assert!(out.contains("!"));
assert!(out.contains("resolve(\"is_visible\")"));
assert!(out.contains("resolve(\"is_hidden\")"));
}
#[test]
fn rewrite_if_expr_comparison_preserved() {
let out = crate::template_codegen::rewrite_if_expr("count > 0");
assert!(out.contains(">"));
assert!(out.contains("resolve(\"count\")"));
assert!(out.contains("parse::<f64>()"));
}
#[test]
fn rewrite_if_expr_comparison_with_logic() {
let out = crate::template_codegen::rewrite_if_expr("count > 0 && is_visible");
assert!(out.contains(">"));
assert!(out.contains("&&"));
assert!(out.contains("resolve(\"count\")"));
assert!(out.contains("parse::<f64>()"));
}
#[test]
fn v_if_else_emits_block_push() {
let tpl = r#"<template>
<div class="app">
<p class="a">{{ x }}</p>
<p v-if="ok" class="b">yes</p>
<p v-else class="c">no</p>
<p class="d">{{ y }}</p>
</div>
</template>"#;
let rust = crate::compile_template_to_rs(tpl, "app", None).expect("compile should succeed");
assert!(
!rust.contains("__children.push((if "),
"v-if/v-else must not be pushed as a parenthesized value: {}",
rust
);
assert!(
!rust.contains("__children.push({"),
"the conditional must not be wrapped in a redundant outer block: {}",
rust
);
assert!(
rust.contains("__children.push(if "),
"expected a bare conditional push: {}",
rust
);
assert!(
rust.contains("\"a\"") && rust.contains("\"d\""),
"siblings dropped by conditional codegen: {}",
rust
);
}
#[test]
fn v_if_else_resolves_to_single_branch() {
let tpl = r#"<template>
<div class="app">
<p v-if="ok" class="b">yes</p>
<p v-else class="c">no</p>
</div>
</template>"#;
let rust = crate::compile_template_to_rs(tpl, "app", None).expect("compile");
assert!(
rust.contains("\"yes\"") && rust.contains("\"no\""),
"branches missing: {}",
rust
);
let pushes = rust.matches("__children.push(if ").count();
assert_eq!(
pushes, 2,
"expected one conditional push per render fn: {}",
rust
);
}
#[test]
fn scoped_style_prefixes_selectors_with_scope_id() {
use crate::codegen::to_stub_rs;
use crate::sfc::{Attr, Sfc, StyleBlock};
let scoped = Sfc {
style: Some(StyleBlock {
attrs: vec![Attr {
name: "scoped".into(),
value: None,
}],
content: ".header h1 { color: red; }\nh1, .btn { margin: 0; }\n".into(),
}),
..Default::default()
};
let out = to_stub_rs(&scoped, "Counter");
assert!(
out.contains("pub const SCOPE_ID: &str = \"data-v-"),
"expected a non-empty scope id: {}",
out
);
assert!(
out.contains(".header[data-v-") && out.contains("] h1[data-v-"),
"descendant selector compounds not scoped: {}",
out
);
assert!(
out.contains("h1[data-v-") && out.contains(".btn[data-v-"),
"selector list members not scoped: {}",
out
);
let unscoped = Sfc {
style: Some(StyleBlock {
attrs: vec![],
content: ".header { color: red; }".into(),
}),
..Default::default()
};
let plain = to_stub_rs(&unscoped, "Counter");
assert!(
plain.contains("pub const SCOPE_ID: &str = \"\";"),
"unscoped component should emit empty scope id: {}",
plain
);
assert!(
plain.contains(".header { color: red; }") && !plain.contains("[data-v-"),
"unscoped style must pass through unchanged: {}",
plain
);
}
use crate::template_codegen::{slot_binding, slot_content, strip_slot_binding};
fn slot_bound(tag: &str, directive: &str, children: Vec<Node>) -> Node {
Node::Element {
tag: tag.to_string(),
attrs: vec![
TemplateAttr {
name: directive.to_string(),
value: None,
kind: AttrKind::Directive,
},
TemplateAttr {
name: "class".to_string(),
value: Some("own".to_string()),
kind: AttrKind::Static,
},
],
children,
self_closing: false,
}
}
fn text_node(t: &str) -> Node {
Node::Text(t.to_string())
}
#[test]
fn a_template_fragment_contributes_its_children_not_itself() {
let node = slot_bound(
"template",
"slot:header",
vec![text_node("a"), text_node("b")],
);
let content = slot_content(&node);
assert_eq!(
content.len(),
2,
"the `<template>` wrapper must be flattened away, leaving its two children \
as the slot's content. Got {content:?}"
);
assert!(matches!(&content[0], Node::Text(t) if t == "a"));
assert!(matches!(&content[1], Node::Text(t) if t == "b"));
}
#[test]
fn a_plain_element_is_its_own_content_and_keeps_its_own_attributes() {
let node = slot_bound("h1", "slot:header", vec![text_node("title")]);
let content = slot_content(&node);
assert_eq!(
content.len(),
1,
"expected the element itself, got {content:?}"
);
let Node::Element { tag, attrs, .. } = &content[0] else {
panic!("expected an element, got {:?}", content[0]);
};
assert_eq!(tag, "h1");
assert!(
attrs
.iter()
.any(|a| a.name == "class" && a.value.as_deref() == Some("own")),
"the element's own `class` must survive the slot pass. Got {attrs:?}"
);
}
#[test]
fn the_slot_binding_is_stripped_and_nothing_else_is() {
let node = slot_bound("p", "slot:footer", vec![text_node("x")]);
let stripped = strip_slot_binding(&node);
let Node::Element { attrs, .. } = &stripped else {
panic!("expected an element, got {stripped:?}");
};
assert!(
!attrs
.iter()
.any(|a| a.kind == AttrKind::Directive && a.name.starts_with("slot:")),
"the `v-slot:` binding must not survive onto the content. Got {attrs:?}"
);
assert_eq!(
attrs.len(),
1,
"only the `slot:` directive goes; every other attribute is markup the \
caller wrote. Got {attrs:?}"
);
}
#[test]
fn the_slot_name_is_read_off_the_directive_unchanged() {
let bound = slot_bound("p", "slot:footer-bar", vec![]);
assert_eq!(
slot_binding(&bound).as_deref(),
Some("footer-bar"),
"the name is taken from the attribute as it arrived, not re-folded. The \
parser already folded it."
);
let unnamed = Node::Element {
tag: "p".to_string(),
attrs: vec![],
children: vec![],
self_closing: false,
};
assert_eq!(
slot_binding(&unnamed),
None,
"a child that names no slot is the default slot, which is `None` here and \
becomes the key `\"default\"` in `slots_map_expr`. Not the string \
`\"default\"`: a child that names a slot called `default` would then be \
indistinguishable from one that names none."
);
}