use super::helpers::{VUE_SETUP_HELPERS, generate_template_context, get_dom_event_type};
use super::{
TemplateGlobal, VirtualTsCheckOptions, VirtualTsGenerationOptions, VirtualTsOptions,
generate_virtual_ts, generate_virtual_ts_with_offsets,
generate_virtual_ts_with_offsets_and_checks, generate_virtual_ts_with_offsets_options_api,
};
fn assert_virtual_ts_snapshot(name: &str, value: &str) {
insta::with_settings!({
snapshot_path => "../../snapshots"
}, {
insta::assert_snapshot!(name, value);
});
}
#[test]
fn test_vue_setup_helpers_are_actual_functions() {
assert_virtual_ts_snapshot("virtual_ts_vue_setup_helpers", VUE_SETUP_HELPERS);
}
#[test]
fn test_vue_template_context() {
let ctx = generate_template_context(&VirtualTsOptions::default());
assert_virtual_ts_snapshot("virtual_ts_vue_template_context", ctx.as_str());
}
#[test]
fn test_vue_template_context_with_globals() {
let options = VirtualTsOptions {
template_globals: vec![
TemplateGlobal {
name: "$t".into(),
type_annotation: "(...args: any[]) => string".into(),
default_value: "(() => '') as any".into(),
},
TemplateGlobal {
name: "$route".into(),
type_annotation: "any".into(),
default_value: "{} as any".into(),
},
],
..Default::default()
};
let ctx = generate_template_context(&options);
assert_virtual_ts_snapshot("virtual_ts_vue_template_context_with_globals", ctx.as_str());
}
fn analyze_options_api_script(script: &str) -> vize_croquis::Croquis {
use vize_croquis::{Analyzer, AnalyzerOptions};
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full()).with_options_api();
analyzer.analyze_script_plain(script);
analyzer.finish()
}
#[test]
fn test_options_api_virtual_ts_emits_this_bridge() {
let script = r#"import { defineComponent } from 'vue'
function useFakeStore() {
return {
ready: false,
items: [] as Array<{ id: number; label: string }>,
}
}
export default defineComponent({
setup() {
const store = useFakeStore()
return { store }
},
data() {
return { count: 0 }
},
computed: {
status() {
return this.store.ready
},
},
methods: {
bump(step: number) {
this.count = this.count + step
return this.status
},
},
props: {
initial: { type: Number, default: 0 },
},
})
"#;
let summary = analyze_options_api_script(script);
let output = generate_virtual_ts_with_offsets_options_api(
&summary,
Some(script),
None,
0,
0,
&Default::default(),
);
assert!(
output.code.contains("type __VizeThis ="),
"expected typed Options API `this` bridge:\n{}",
output.code
);
assert!(
output.code.contains("__vize_method_bump"),
"expected method body to be checked through a typed wrapper:\n{}",
output.code
);
assert!(
output.code.contains("__vize_computed_status"),
"expected computed body to be checked through a typed wrapper:\n{}",
output.code
);
}
#[test]
fn test_options_api_virtual_ts_emits_typed_shape_for_pinia_spread_helpers() {
let script = r#"import { defineComponent } from 'vue'
import { mapState, mapActions } from 'pinia'
function useFakeStore() {
return {
ready: false,
items: [] as Array<{ id: number; label: string }>,
setReady(_v: boolean) {},
}
}
export default defineComponent({
computed: {
...mapState(useFakeStore, ['items', 'ready']),
localComputed() { return 1 },
},
methods: {
...mapActions(useFakeStore, ['setReady']),
},
})
"#;
let summary = analyze_options_api_script(script);
let output = generate_virtual_ts_with_offsets_options_api(
&summary,
Some(script),
None,
0,
0,
&Default::default(),
);
assert!(
output
.code
.contains("[K in 'items' | 'ready']: ReturnType<typeof useFakeStore>[K]"),
"expected precise mapped type for mapState spread keys:\n{}",
output.code
);
assert!(
output
.code
.contains("[K in 'setReady']: ReturnType<typeof useFakeStore>[K]"),
"expected precise mapped type for mapActions spread keys:\n{}",
output.code
);
}
#[test]
fn test_script_setup_output_does_not_emit_options_api_bridge() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref } from 'vue'
const count = ref(0)
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
let summary = analyzer.finish();
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &Default::default());
assert!(
!output.code.contains("__VizeThis"),
"`<script setup>` output must not contain the Options API bridge:\n{}",
output.code
);
assert!(
!output.code.contains("__vize_method_") && !output.code.contains("__vize_computed_"),
"`<script setup>` output must not contain Options API wrappers:\n{}",
output.code
);
}
#[test]
fn test_const_auto_import_stubs_skip_imported_names() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { currentUser } from './users'
const count = 1
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
let summary = analyzer.finish();
let options = VirtualTsOptions {
auto_import_stubs: vec![
"declare const currentUser: any;".into(),
"declare const useHydratedHead: any;".into(),
],
..Default::default()
};
let output = generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &options);
assert_virtual_ts_snapshot(
"virtual_ts_auto_import_stubs_skip_imported_names",
output.code.as_str(),
);
}
#[test]
fn test_external_template_bindings_do_not_shadow_auto_imported_components() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = "const count = 'oops'\n";
let template = r#"<AutoCard :count="count" />"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let options = VirtualTsOptions {
auto_import_stubs: vec![
"declare const AutoCard: typeof import('./components/AutoCard.vue.ts')['default'];"
.into(),
],
external_template_bindings: vec!["AutoCard".into()],
..Default::default()
};
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), Some(&root), 0, 0, &options);
assert!(
output
.code
.contains("declare const AutoCard: typeof import")
);
assert!(!output.code.contains("const AutoCard: any"));
assert!(
output
.code
.contains("type __AutoCard_Props_0 = typeof AutoCard")
);
}
#[test]
fn test_unresolved_component_props_are_not_checked() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = "const count = 'unknown'\n";
let template = r#"<AutoCard :count="count" />"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts_with_offsets(
&summary,
Some(script),
Some(&root),
0,
0,
&VirtualTsOptions::default(),
);
assert!(output.code.contains("const AutoCard: any"));
assert!(!output.code.contains("type __AutoCard_Props_0"));
assert!(!output.code.contains("__AutoCard_Check_0"));
}
#[test]
fn test_template_instance_globals_delegate_to_component_public_instance() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let template = r#"<button :title="$t('hello')">{{ missing }}</button>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts_with_offsets(
&summary,
None,
Some(&root),
0,
0,
&VirtualTsOptions::default(),
);
assert!(
output
.code
.contains("const $t: __VizeInstanceGlobal<'$t'> = undefined as any;"),
"{}",
output.code
);
assert!(output.code.contains("void ($t('hello'));"));
assert!(output.code.contains("void (missing);"));
assert!(!output.code.contains("void ($t);"));
let configured_output = generate_virtual_ts_with_offsets(
&summary,
None,
Some(&root),
0,
0,
&VirtualTsOptions {
template_globals: vec![TemplateGlobal {
name: "$t".into(),
type_annotation: "(key: string) => string".into(),
default_value: "(() => '') as any".into(),
}],
..Default::default()
},
);
assert!(
!configured_output
.code
.contains("__VizeInstanceGlobal<'$t'>")
);
assert!(
configured_output
.code
.contains("const $t: __Global<'$t', (key: string) => string>")
);
}
#[test]
fn test_template_instance_globals_skip_setup_bindings() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"function functionCall(): any {}
const $q = functionCall()
"#;
let template = r#"<div v-if="$q">None</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
!output
.code
.contains("const $q: __VizeInstanceGlobal<'$q'> = undefined as any;"),
"setup binding named like an instance global must not be redeclared:\n{}",
output.code
);
assert!(
output.code.contains("if (($q))"),
"template expression should still resolve the setup binding:\n{}",
output.code
);
}
#[test]
fn test_define_expose_is_part_of_component_instance() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"defineExpose({
hide: () => {
console.log()
},
})
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), None, 0);
assert!(
output.code.contains(
"export type Exposed = Awaited<ReturnType<typeof __setup>>[\"__vize_exposed\"];"
),
"runtime defineExpose should emit an Exposed type:\n{}",
output.code
);
assert!(
output.code.contains("type __VizeComponentInstance = {\n $props: Props;\n $emit: __EmitFn<Emits>;\n $slots: Slots;\n} & Exposed;"),
"component instance should include exposed bindings:\n{}",
output.code
);
}
#[test]
fn test_kebab_case_component_names_are_sanitized_in_type_helpers() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"const value = 'hello'
function handleUpdate(value: string) {
void value
}
"#;
let template = r#"<my-widget :label="value" @update:model-value="handleUpdate" />"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts_with_offsets(
&summary,
Some(script),
Some(&root),
0,
0,
&Default::default(),
);
assert_virtual_ts_snapshot(
"virtual_ts_kebab_case_component_names",
output.code.as_str(),
);
}
#[test]
fn test_check_props_option_disables_component_prop_checks() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import Child from './Child.vue'
const wrong = 'not a number'
"#;
let template = r#"<Child :count="wrong" />"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts_with_offsets_and_checks(
&summary,
Some(script),
Some(&root),
0,
0,
&VirtualTsOptions::default(),
VirtualTsGenerationOptions {
check_options: VirtualTsCheckOptions {
check_props: false,
..Default::default()
},
..Default::default()
},
);
assert!(!output.code.contains("__vize_prop_check"));
assert!(!output.code.contains("type __Child_Props_0"));
}
#[test]
fn test_check_template_bindings_option_disables_template_expressions() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = "const message = 'hello'\n";
let template = r#"<div>{{ message }}</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts_with_offsets_and_checks(
&summary,
Some(script),
Some(&root),
0,
0,
&VirtualTsOptions::default(),
VirtualTsGenerationOptions {
check_options: VirtualTsCheckOptions {
check_template_bindings: false,
..Default::default()
},
..Default::default()
},
);
assert!(!output.code.contains("void (message);"));
}
#[test]
fn test_dom_event_type_mapping() {
assert_eq!(get_dom_event_type("dblclick"), "MouseEvent");
assert_eq!(get_dom_event_type("mousedown"), "MouseEvent");
assert_eq!(get_dom_event_type("mouseup"), "MouseEvent");
assert_eq!(get_dom_event_type("mousemove"), "MouseEvent");
assert_eq!(get_dom_event_type("contextmenu"), "MouseEvent");
assert_eq!(get_dom_event_type("click"), "PointerEvent");
assert_eq!(get_dom_event_type("auxclick"), "PointerEvent");
assert_eq!(get_dom_event_type("pointerdown"), "PointerEvent");
assert_eq!(get_dom_event_type("pointerup"), "PointerEvent");
assert_eq!(get_dom_event_type("touchstart"), "TouchEvent");
assert_eq!(get_dom_event_type("touchend"), "TouchEvent");
assert_eq!(get_dom_event_type("keydown"), "KeyboardEvent");
assert_eq!(get_dom_event_type("keyup"), "KeyboardEvent");
assert_eq!(get_dom_event_type("keypress"), "KeyboardEvent");
assert_eq!(get_dom_event_type("focus"), "FocusEvent");
assert_eq!(get_dom_event_type("blur"), "FocusEvent");
assert_eq!(get_dom_event_type("input"), "InputEvent");
assert_eq!(get_dom_event_type("beforeinput"), "InputEvent");
assert_eq!(get_dom_event_type("submit"), "SubmitEvent");
assert_eq!(get_dom_event_type("change"), "Event");
assert_eq!(get_dom_event_type("drag"), "DragEvent");
assert_eq!(get_dom_event_type("drop"), "DragEvent");
assert_eq!(get_dom_event_type("copy"), "ClipboardEvent");
assert_eq!(get_dom_event_type("paste"), "ClipboardEvent");
assert_eq!(get_dom_event_type("wheel"), "WheelEvent");
assert_eq!(get_dom_event_type("animationstart"), "AnimationEvent");
assert_eq!(get_dom_event_type("animationend"), "AnimationEvent");
assert_eq!(get_dom_event_type("transitionend"), "TransitionEvent");
assert_eq!(get_dom_event_type("customEvent"), "Event");
assert_eq!(get_dom_event_type("unknown"), "Event");
}
#[test]
fn test_vfor_destructuring_scope() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref } from 'vue'
const items = ref([{ id: 1, name: 'Hello' }])
"#;
let template = r#"<ul>
<li v-for="{ id, name } in items" :key="id">
{{ id }}: {{ name }}
</li>
</ul>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert_virtual_ts_snapshot("virtual_ts_vfor_destructuring_scope", output.code.as_str());
}
#[test]
fn test_nested_vif_velse_chain() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref } from 'vue'
const status = ref('loading')
const message = ref('')
"#;
let template = r#"<div>
<div v-if="status === 'loading'">Loading</div>
<div v-else-if="status === 'error'">{{ message }}</div>
<div v-else>Done</div>
</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert_virtual_ts_snapshot("virtual_ts_nested_vif_velse_chain", output.code.as_str());
}
#[test]
fn test_v_else_if_chain_uses_linear_control_flow() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"type Log =
| { type: 't0'; info: { value0: string } }
| { type: 't1'; info: { value1: string } }
| { type: 't2'; info: { value2: string } }
defineProps<{ log: Log }>()
"#;
let template = r#"<div>
<span v-if="log.type === 't0'">{{ log.info.value0 }}</span>
<span v-else-if="log.type === 't1'">{{ log.info.value1 }}</span>
<span v-else-if="log.type === 't2'">{{ log.info.value2 }}</span>
</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
output.code.contains("if (log.type === 't0') {"),
"expected first branch to use native control flow:\n{}",
output.code
);
assert!(
output.code.contains("} else if (log.type === 't1') {")
&& output.code.contains("} else if (log.type === 't2') {"),
"expected else-if branches to use native control flow:\n{}",
output.code
);
assert!(
!output.code.contains("!(log.type === 't0') &&"),
"virtual TS should not repeat cumulative negated branch guards:\n{}",
output.code
);
assert!(
!output.code.contains("void (log.type === 't1'); // VIf"),
"branch conditions should not be emitted again inside the branch body:\n{}",
output.code
);
}
#[test]
fn test_scoped_slot_expressions() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import MyList from './MyList.vue'
const items = ['a', 'b']
"#;
let template = r#"<MyList :items="items">
<template #default="{ item }">
{{ item }}
</template>
</MyList>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert_virtual_ts_snapshot("virtual_ts_scoped_slot_expressions", output.code.as_str());
}
#[test]
fn test_v_if_narrows_nullable_binding() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"interface User { name: string }
const user: User | null = null as any
"#;
let template = r#"<div v-if="user">
<p>{{ user.name }}</p>
</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
output.code.contains("if ((user))"),
"expected `if ((user))` narrowing wrapper in virtual TS, got:\n{}",
output.code
);
}
#[test]
fn test_reserved_prop_and_hyphenated_slot_names() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import TrendChart from './TrendChart.vue'
defineProps<{
class?: string
}>()
"#;
let template = r#"<TrendChart :class="class">
<template #area-gradient="{ id }">
{{ id }}
</template>
</TrendChart>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
let expression_start = template.find("\"class\"").unwrap() + 1;
let expression_end = expression_start + "class".len();
let mapping = output
.mappings
.iter()
.find(|mapping| mapping.src_range == (expression_start..expression_end))
.expect("should map the rewritten class prop expression");
assert_eq!(&output.code[mapping.gen_range.clone()], "props[\"class\"]");
assert_virtual_ts_snapshot(
"virtual_ts_reserved_prop_and_hyphenated_slot_names",
output.code.as_str(),
);
}
#[test]
fn test_multiple_event_handlers() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref } from 'vue'
const count = ref(0)
function handleClick() { count.value++ }
function handleHover() {}
"#;
let template = r#"<div>
<button @click="handleClick" @mouseenter="handleHover">{{ count }}</button>
</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert_virtual_ts_snapshot("virtual_ts_multiple_event_handlers", output.code.as_str());
}
#[test]
fn test_v_if_guard_wraps_same_element_event_handler() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"type UnionType = { type: "a" } | { type: "b", bSpecific: () => void }
const val = 0 as unknown as UnionType;
"#;
let template = r#"<div v-if="val.type === 'b'" @click="val.bSpecific"></div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
output
.code
.contains("if ((val.type === 'b')) {\n const __vize_handler"),
"same-element event handler should preserve the v-if guard while type-checking the handler:\n{}",
output.code
);
}
#[test]
fn test_inline_arrow_event_handler_is_called_with_event() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let template = r#"<button @click="(payload) => console.log(payload)">Click</button>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, None, Some(&root), 0);
assert!(
output
.code
.contains("((payload) => console.log(payload))($event);"),
"inline arrow handler should be invoked with the event:\n{}",
output.code
);
assert!(
!output
.code
.contains("(payload) => console.log(payload); // handler expression"),
"inline arrow handler must not be emitted as a void expression:\n{}",
output.code
);
}
#[test]
fn test_computed_member_event_handler_reference_is_called_with_event() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"const handlers = { x: 42 } as const
const arr = [(event: PointerEvent) => event.preventDefault()]
"#;
let template =
r#"<button @click="handlers['x']">Bad</button><button @click="arr[0]">Good</button>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
output.code.contains(
"((handler: ($event: PointerEvent) => unknown) => handler)((handlers['x']));"
),
"computed-member handler references should be checked as callable:\n{}",
output.code
);
assert!(
output
.code
.contains("((handler: ($event: PointerEvent) => unknown) => handler)((arr[0]));"),
"index handler references should be checked as callable:\n{}",
output.code
);
assert!(
!output
.code
.contains("handlers['x']; // handler expression"),
"computed-member handler must not be emitted as a bare statement:\n{}",
output.code
);
}
#[test]
fn test_component_event_fallback_uses_dom_event_type_only_in_quirks() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import Child from './Child.vue'
function eventHandler(event: Event) {
void event
}
"#;
let template = r#"<Child @keydown="eventHandler" />"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let standard_output = generate_virtual_ts_with_offsets(
&summary,
Some(script),
Some(&root),
0,
0,
&VirtualTsOptions::default(),
);
assert!(
standard_output.code.contains("? __A : unknown[]"),
"standard component event fallback should stay unknown:\n{}",
standard_output.code
);
assert!(
!standard_output.code.contains("[KeyboardEvent]"),
"standard component event fallback must not use DOM event types:\n{}",
standard_output.code
);
let quirks_output = generate_virtual_ts_with_offsets_and_checks(
&summary,
Some(script),
Some(&root),
0,
0,
&VirtualTsOptions::default(),
VirtualTsGenerationOptions {
template_syntax_quirks: true,
..Default::default()
},
);
assert!(
quirks_output.code.contains("unknown[] extends __Child_")
&& quirks_output.code.contains("? KeyboardEvent : __Child_"),
"quirks component event fallback should use the DOM event type when args stay unknown:\n{}",
quirks_output.code
);
}
#[test]
fn test_multiline_statement_event_handler_uses_handler_scope() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"const keys = ['a']
function selectWord(key: string) {}
function editWord() {}
"#;
let template = r#"<button
v-for="key in keys"
@click.stop="
selectWord(key);
editWord();
"
>edit</button>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
output.code.contains("// @click handler"),
"statement-list handlers should get an event handler scope:\n{}",
output.code
);
assert!(
output.code.contains("selectWord(key);") && output.code.contains("editWord();"),
"handler statements should be preserved:\n{}",
output.code
);
assert!(
!output.code.contains("void (selectWord(key);")
&& !output.code.contains("void (\n selectWord(key);"),
"statement-list handlers must not be emitted as parenthesized expressions:\n{}",
output.code
);
}
#[test]
fn test_object_form_v_on_is_preserved_as_expression() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"const props = defineProps<{
handlers?: {
'update:modelValue'?: () => void
}
}>()
"#;
let template = r#"<button v-on="{ 'update:modelValue': props.handlers?.['update:modelValue'] }">Click</button>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
output.code.contains(
"void ({ 'update:modelValue': props.handlers?.['update:modelValue'] }); // VOn"
),
"object-form v-on should be emitted as an expression:\n{}",
output.code
);
assert!(
!output.code.contains("@unknown handler"),
"object-form v-on must not create a synthetic event handler:\n{}",
output.code
);
}
#[test]
fn test_source_mappings_generated() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref } from 'vue'
const msg = ref('Hello')
"#;
let template = r#"<div>{{ msg }}</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(
!output.mappings.is_empty(),
"Should generate source mappings for template expressions"
);
for mapping in &output.mappings {
assert!(
mapping.gen_range.start < mapping.gen_range.end,
"Generated range should be non-empty"
);
assert!(
mapping.src_range.start < mapping.src_range.end,
"Source range should be non-empty"
);
}
}
#[test]
fn test_source_mappings_target_expression_text() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { useTemplateRef } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('input')
"#;
let template = r#"<div :data-active="inputRef && inputRef.focus()"></div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
let expression = "inputRef && inputRef.focus()";
let source_start = template.find(expression).unwrap();
let source_end = source_start + expression.len();
let mapping = output
.mappings
.iter()
.find(|mapping| mapping.src_range == (source_start..source_end))
.expect("should map the template expression");
assert_eq!(&output.code[mapping.gen_range.clone()], expression);
}
#[test]
fn test_template_shadow_bindings_only_unwrap_vue_refs() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref, useTemplateRef } from 'vue'
const users = ref([{ id: 1 }])
const inputRef = useTemplateRef<HTMLInputElement>('input')
"#;
let template = r#"<div>{{ users.length }} {{ inputRef && inputRef.focus() }}</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert_virtual_ts_snapshot("virtual_ts_template_binding_unwraps", output.code.as_str());
}
#[test]
fn test_virtual_ts_generation_survives_unicode_script_comments() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"const reasgnSubMenuOpen = debounce(() => {
console.log(1222222222222222222222222222222);
}, 100);
// あいうえおかきくけこさしすせそたちつてとなにぬねの
const heightLimit = "65vh";
// はひふへほまみむめもやいゆえよらりるれろわをん
"#;
let template = r#"<div>{{ heightLimit }}</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert!(output.code.contains("heightLimit"));
}
#[test]
fn test_script_setup_generic_param_injected_into_hoisted_type() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"type Option = { key: T; label: string }
defineProps<{
options: Option[]
current: T | undefined
}>()
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup_with_generic(script, Some("T extends string"));
let summary = analyzer.finish();
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &Default::default());
let (module_scope, _setup_scope) = output
.code
.split_once("// ========== Setup Scope ==========")
.expect("setup scope marker present");
assert!(
module_scope.contains("type Option<T extends string = any> = { key: T; label: string }"),
"hoisted type should gain the SFC generic parameter so `T` resolves at module scope:\n{}",
output.code
);
}
#[test]
fn test_script_setup_type_reexport_lifted_to_module_scope() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { type FilterType } from './ReExportType'
export type { FilterType }
defineProps<{ kind?: FilterType }>()
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
let summary = analyzer.finish();
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &Default::default());
let (module_scope, setup_scope) = output
.code
.split_once("// ========== Setup Scope ==========")
.expect("setup scope marker present");
assert!(
module_scope.contains("export type { FilterType }"),
"re-export should be lifted to module scope:\n{}",
output.code
);
assert!(
!setup_scope.contains("export type { FilterType }"),
"re-export must not be trapped inside __setup():\n{}",
output.code
);
}
#[test]
fn test_vfor_component_props_in_scope() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref } from 'vue'
import TodoItem from './TodoItem.vue'
const todos = ref([{ id: 1, text: 'Hello' }])
"#;
let template = r#"<div>
<TodoItem v-for="todo in todos" :key="todo.id" :item="todo" />
</div>"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert_virtual_ts_snapshot(
"virtual_ts_vfor_component_props_in_scope",
output.code.as_str(),
);
}
#[test]
fn test_component_prop_checks_respect_same_element_vif_guard() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { ref } from 'vue'
import LinkComp from './LinkComp.vue'
const item = ref<{ name: string } | undefined>()
"#;
let template = r#"<LinkComp v-if="item" :to="item.name" />"#;
let allocator = vize_carton::Bump::new();
let (root, _) = vize_armature::parse(&allocator, template);
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(script);
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let output = generate_virtual_ts(&summary, Some(script), Some(&root), 0);
assert_virtual_ts_snapshot(
"virtual_ts_component_prop_checks_respect_same_element_vif_guard",
output.code.as_str(),
);
}
#[test]
fn test_plain_options_object_default_export_is_wrapped_with_define_component() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"export default {
data() {
return { count: 0 }
},
computed: {
doubled() {
return this.count * 2
},
},
}
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_plain(script);
let summary = analyzer.finish();
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &Default::default());
assert!(
output
.code
.contains("declare const __vizeDefineComponent: typeof import('vue').defineComponent;"),
"expected the defineComponent helper declaration:\n{}",
output.code
);
assert!(
output
.code
.contains("const __default__ = __vizeDefineComponent({"),
"expected the options object to be wrapped with defineComponent:\n{}",
output.code
);
assert!(
output.code.contains("\n })\n"),
"expected the wrap to be closed after the options object:\n{}",
output.code
);
}
#[test]
fn test_single_line_options_object_default_export_wrap_keeps_trailing_text() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = "export default { name: 'Foo' };\n";
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_plain(script);
let summary = analyzer.finish();
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &Default::default());
assert!(
output
.code
.contains("const __default__ = __vizeDefineComponent({ name: 'Foo' });"),
"expected a single-line wrap that preserves the trailing semicolon:\n{}",
output.code
);
}
#[test]
fn test_define_component_default_export_is_not_double_wrapped() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"import { defineComponent } from 'vue'
export default defineComponent({
data() {
return { count: 0 }
},
})
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_plain(script);
let summary = analyzer.finish();
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &Default::default());
assert!(
!output.code.contains("__vizeDefineComponent"),
"an explicit defineComponent call must not be re-wrapped:\n{}",
output.code
);
assert!(
output
.code
.contains("const __default__ = defineComponent({"),
"expected the existing rewrite to stay untouched:\n{}",
output.code
);
}
#[test]
fn test_non_object_default_export_is_not_wrapped() {
use vize_croquis::{Analyzer, AnalyzerOptions};
let script = r#"const component = { name: 'Foo' }
export default component
"#;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_plain(script);
let summary = analyzer.finish();
let output =
generate_virtual_ts_with_offsets(&summary, Some(script), None, 0, 0, &Default::default());
assert!(
!output.code.contains("__vizeDefineComponent"),
"identifier default exports must not be wrapped:\n{}",
output.code
);
assert!(
output.code.contains("const __default__ = component"),
"expected the existing rewrite to stay untouched:\n{}",
output.code
);
}