mod common;
mod fixtures;
use common::{TestGenerator, TestProject};
#[test]
fn test_vanilla_typescript_full_pipeline() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub name: String,
pub email: String,
}
#[tauri::command]
pub fn get_user(user_id: String) -> Result<User, String> {
Ok(User {
id: user_id,
name: "Test User".to_string(),
email: "test@example.com".to_string(),
})
}
#[tauri::command]
pub fn create_user(name: String, email: String) -> Result<User, String> {
Ok(User {
id: "123".to_string(),
name,
email,
})
}
"#,
);
let (analyzer, commands) = project.analyze();
assert_eq!(commands.len(), 2);
let generator = TestGenerator::new();
let files = generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("none"),
None,
);
assert!(files.contains(&"types.ts".to_string()));
assert!(files.contains(&"commands.ts".to_string()));
assert!(files.contains(&"index.ts".to_string()));
let types = generator.read_file("types.ts");
assert!(types.contains("export interface User"));
assert!(types.contains("id: string"));
assert!(types.contains("name: string"));
assert!(types.contains("email: string"));
assert!(!types.contains("z.object"));
assert!(!types.contains("UserSchema"));
let commands_file = generator.read_file("commands.ts");
assert!(commands_file.contains("export async function getUser"));
assert!(commands_file.contains("export async function createUser"));
assert!(commands_file.contains("invoke"));
let index = generator.read_file("index.ts");
assert!(index.contains("export * from './types'"));
assert!(index.contains("export * from './commands'"));
}
#[test]
fn test_zod_typescript_full_pipeline() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Product {
pub id: String,
pub name: String,
pub price: f64,
pub in_stock: bool,
}
#[tauri::command]
pub fn get_product(product_id: String) -> Result<Product, String> {
Ok(Product {
id: product_id,
name: "Widget".to_string(),
price: 19.99,
in_stock: true,
})
}
#[tauri::command]
pub fn create_product(name: String, price: f64) -> Result<Product, String> {
Ok(Product {
id: "123".to_string(),
name,
price,
in_stock: true,
})
}
"#,
);
let (analyzer, commands) = project.analyze();
assert_eq!(commands.len(), 2);
let generator = TestGenerator::new();
let files = generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("zod"),
None,
);
assert!(files.contains(&"types.ts".to_string()));
assert!(files.contains(&"commands.ts".to_string()));
assert!(files.contains(&"index.ts".to_string()));
let types = generator.read_file("types.ts");
assert!(
types.contains("ProductSchema"),
"Should generate ProductSchema"
);
assert!(
types.contains("z.object") || types.contains("z.string"),
"Should use zod validators"
);
assert!(
types.contains("Product") && types.contains("export"),
"Should export Product type"
);
let commands_file = generator.read_file("commands.ts");
assert!(commands_file.contains("getProduct") || commands_file.contains("get_product"));
assert!(commands_file.contains("createProduct") || commands_file.contains("create_product"));
}
#[test]
fn test_complete_app_with_commands_channels_and_events() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use tauri::Manager;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: String,
pub content: String,
pub timestamp: u64,
}
#[tauri::command]
pub fn send_message(
content: String,
app: tauri::AppHandle,
on_progress: tauri::ipc::Channel<f32>,
) -> Result<Message, String> {
let msg = Message {
id: "123".to_string(),
content,
timestamp: 1234567890,
};
// Emit event
app.emit("message-sent", msg.clone()).ok();
// Send channel progress
on_progress.send(100.0).ok();
Ok(msg)
}
"#,
);
let (analyzer, commands) = project.analyze();
let events = analyzer.get_discovered_events();
let all_channels = analyzer.get_all_discovered_channels(&commands);
assert_eq!(commands.len(), 1);
assert_eq!(events.len(), 1);
assert_eq!(all_channels.len(), 1);
let generator = TestGenerator::new();
let files = generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("zod"),
None,
);
assert!(files.contains(&"types.ts".to_string()));
assert!(files.contains(&"commands.ts".to_string()));
assert!(files.contains(&"events.ts".to_string()));
assert!(files.contains(&"index.ts".to_string()));
let types = generator.read_file("types.ts");
assert!(types.contains("Message"), "Should contain Message type");
assert!(
types.contains("MessageSchema"),
"Should contain MessageSchema"
);
assert!(
types.contains("Channel") || types.contains("channel"),
"Should reference Channel"
);
let commands_file = generator.read_file("commands.ts");
assert!(
commands_file.contains("sendMessage"),
"Should contain sendMessage function"
);
assert!(
commands_file.contains("Channel") || commands_file.contains("channel"),
"Should use Channel in signature"
);
let events_file = generator.read_file("events.ts");
assert!(
events_file.contains("MessageSent") || events_file.contains("message"),
"Should contain message-sent event listener"
);
let index = generator.read_file("index.ts");
assert!(index.contains("export * from './types'"));
assert!(index.contains("export * from './commands'"));
assert!(index.contains("export * from './events'"));
}
#[test]
fn test_serde_rename_full_pipeline() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserProfile {
pub user_id: String,
pub first_name: String,
pub last_name: String,
#[serde(rename = "emailAddress")]
pub email: String,
}
#[tauri::command]
#[serde(rename_all = "camelCase")]
pub fn get_profile(user_id: String) -> Result<UserProfile, String> {
Ok(UserProfile {
user_id,
first_name: "John".to_string(),
last_name: "Doe".to_string(),
email: "john@example.com".to_string(),
})
}
"#,
);
let (analyzer, commands) = project.analyze();
let generator = TestGenerator::new();
generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("zod"),
None,
);
let types = generator.read_file("types.ts");
assert!(
types.contains("userId"),
"Should use userId from camelCase rename"
);
assert!(
types.contains("firstName"),
"Should use firstName from camelCase rename"
);
assert!(
types.contains("lastName"),
"Should use lastName from camelCase rename"
);
assert!(
types.contains("emailAddress"),
"Should use emailAddress from explicit rename"
);
}
#[test]
fn test_empty_project_no_generation() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
fn main() {
println!("Hello, world!");
}
"#,
);
let (analyzer, commands) = project.analyze();
assert_eq!(commands.len(), 0);
let generator = TestGenerator::new();
let files = generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("none"),
None,
);
assert!(files.contains(&"index.ts".to_string()));
}
#[test]
fn test_deeply_nested_types_full_pipeline() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Address {
pub street: String,
pub city: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
pub email: String,
pub address: Address,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub contacts: Vec<Contact>,
}
#[tauri::command]
pub fn get_user_map() -> HashMap<String, User> {
HashMap::new()
}
"#,
);
let (analyzer, commands) = project.analyze();
let generator = TestGenerator::new();
generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("none"),
None,
);
let types = generator.read_file("types.ts");
assert!(types.contains("export interface Address"));
assert!(types.contains("export interface Contact"));
assert!(types.contains("export interface User"));
assert!(types.contains("address: Address"));
assert!(types.contains("contacts: Contact[]") || types.contains("contacts: Array<Contact>"));
let commands_file = generator.read_file("commands.ts");
assert!(
commands_file.contains("Record<string, User>")
|| commands_file.contains("{ [key: string]: User }")
);
}
#[test]
fn test_event_payload_discovery_from_helper_function() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use tauri::{AppHandle, Manager};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgressUpdate {
pub task_id: String,
pub progress: f64,
pub message: String,
}
/// Command that uses a helper function to emit events
#[tauri::command]
pub async fn process_task(app: AppHandle, task_id: String) -> Result<String, String> {
let update = ProgressUpdate {
task_id: task_id.clone(),
progress: 50.0,
message: "Processing".to_string(),
};
emit_progress(app, &update);
Ok(format!("Task {} completed", task_id))
}
/// Helper function that emits the event with a reference parameter
pub fn emit_progress(app: AppHandle, update: &ProgressUpdate) {
app.emit("progress-update", update).unwrap();
}
"#,
);
let (analyzer, commands) = project.analyze();
let events = analyzer.get_discovered_events();
assert_eq!(commands.len(), 1);
assert_eq!(events.len(), 1, "Should discover one event");
assert_eq!(events[0].event_name, "progress-update");
assert_eq!(
events[0].payload_type, "ProgressUpdate",
"Should infer ProgressUpdate type from function parameter, got: {}",
events[0].payload_type
);
let generator = TestGenerator::new();
generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("zod"),
None,
);
let types = generator.read_file("types.ts");
assert!(
types.contains("ProgressUpdate"),
"Should include ProgressUpdate in types.ts. Got:\n{}",
types
);
let events_file = generator.read_file("events.ts");
assert!(
events_file.contains("ProgressUpdate"),
"Should reference ProgressUpdate in events.ts. Got:\n{}",
events_file
);
}
#[test]
fn test_complex_enum_typescript_generation() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Message {
Quit,
Move(i32, i32),
Write(String),
ChangeColor { r: u8, g: u8, b: u8 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Status {
Active,
Inactive,
Pending,
}
#[tauri::command]
pub fn send_message(msg: Message) -> Result<Status, String> {
Ok(Status::Active)
}
"#,
);
let (analyzer, commands) = project.analyze();
let generator = TestGenerator::new();
generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("none"),
None,
);
let types = generator.read_file("types.ts");
assert!(
types.contains(r#"export type Status = "Active" | "Inactive" | "Pending";"#),
"Simple enum should use string literal union. Got:\n{}",
types
);
assert!(
types.contains("export type Message ="),
"Complex enum should have type declaration. Got:\n{}",
types
);
assert!(
types.contains(r#"type: "Quit""#),
"Should have Quit variant. Got:\n{}",
types
);
assert!(
types.contains(r#"type: "Move""#),
"Should have Move variant. Got:\n{}",
types
);
assert!(
types.contains(r#"type: "Write""#),
"Should have Write variant. Got:\n{}",
types
);
assert!(
types.contains(r#"type: "ChangeColor""#),
"Should have ChangeColor variant. Got:\n{}",
types
);
assert!(
types.contains("data:"),
"Tuple variants should have data field. Got:\n{}",
types
);
assert!(
types.contains("r: number"),
"Struct variant should have r field. Got:\n{}",
types
);
}
#[test]
fn test_complex_enum_zod_generation() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Action {
Start,
Move { x: i32, y: i32 },
Send(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Status {
Active,
Inactive,
}
#[tauri::command]
pub fn perform_action(action: Action) -> Result<Status, String> {
Ok(Status::Active)
}
"#,
);
let (analyzer, commands) = project.analyze();
let generator = TestGenerator::new();
generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("zod"),
None,
);
let types = generator.read_file("types.ts");
assert!(
types.contains("StatusSchema = z.enum"),
"Simple enum should use z.enum. Got:\n{}",
types
);
assert!(
types.contains(r#"["Active", "Inactive"]"#),
"Simple enum should list variants. Got:\n{}",
types
);
assert!(
types.contains("ActionSchema = z.discriminatedUnion"),
"Complex enum should use z.discriminatedUnion. Got:\n{}",
types
);
assert!(
types.contains(r#"z.discriminatedUnion("kind""#),
"Should use 'kind' as discriminator. Got:\n{}",
types
);
assert!(
types.contains(r#"z.literal("Start")"#),
"Should have Start variant. Got:\n{}",
types
);
assert!(
types.contains(r#"z.literal("Move")"#),
"Should have Move variant. Got:\n{}",
types
);
assert!(
types.contains("x:") && types.contains("y:"),
"Move variant should have x and y fields. Got:\n{}",
types
);
assert!(
types.contains(r#"z.literal("Send")"#),
"Should have Send variant. Got:\n{}",
types
);
assert!(
types.contains("data:"),
"Tuple variant should have data field. Got:\n{}",
types
);
assert!(
types.contains("export type Action = z.infer<typeof ActionSchema>"),
"Should export inferred Action type. Got:\n{}",
types
);
assert!(
types.contains("export type Status = z.infer<typeof StatusSchema>"),
"Should export inferred Status type. Got:\n{}",
types
);
}
#[test]
fn test_fully_qualified_types_with_nested_structs() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
use serde::{Deserialize, Serialize};
/// Simulates protobuf-generated field type with full qualification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RtpParameters {
pub mid: String,
pub codecs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsumedResponse {
pub consumer_id: String,
pub producer_id: String,
pub kind: String,
/// This field uses fully qualified Option type as in protobuf generated code
pub rtp_parameters: ::core::option::Option<RtpParameters>,
}
#[tauri::command]
pub fn get_consumed_response() -> Result<ConsumedResponse, String> {
Ok(ConsumedResponse {
consumer_id: "123".to_string(),
producer_id: "456".to_string(),
kind: "audio".to_string(),
rtp_parameters: None,
})
}
"#,
);
let (analyzer, commands) = project.analyze();
assert_eq!(commands.len(), 1);
let generator = TestGenerator::new();
generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("none"),
None,
);
let types = generator.read_file("types.ts");
assert!(
types.contains("export interface ConsumedResponse"),
"Should generate ConsumedResponse type. Got:\n{}",
types
);
assert!(
types.contains("export interface RtpParameters"),
"Should generate RtpParameters type (nested struct). Got:\n{}",
types
);
assert!(
types.contains("rtp_parameters?:") && types.contains("RtpParameters"),
"ConsumedResponse should have rtp_parameters field of type RtpParameters. Got:\n{}",
types
);
assert!(
types.contains("mid:") && types.contains("codecs:"),
"RtpParameters should have mid and codecs fields. Got:\n{}",
types
);
}
#[test]
fn test_cfg_gated_duplicate_command_emitted_once() {
let project = TestProject::new();
project.write_file(
"main.rs",
r#"
#[tauri::command]
#[cfg(target_os = "windows")]
pub fn cmd_update_app_icon(variant: String) -> Result<(), String> {
Ok(())
}
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub fn cmd_update_app_icon(variant: String) -> Result<(), String> {
Err("Only supported on Windows".into())
}
"#,
);
let (analyzer, commands) = project.analyze();
let generator = TestGenerator::new();
generator.generate(
&commands,
analyzer.get_discovered_structs(),
&analyzer,
Some("zod"),
None,
);
let commands_ts = generator.read_file("commands.ts");
let fn_occurrences = commands_ts
.matches("export async function cmdUpdateAppIcon")
.count();
assert_eq!(
fn_occurrences, 1,
"cfg-gated command should be emitted exactly once. Got:\n{}",
commands_ts
);
let types_ts = generator.read_file("types.ts");
let params_occurrences = types_ts
.matches("export const CmdUpdateAppIconParamsSchema")
.count();
assert_eq!(
params_occurrences, 1,
"cfg-gated command's Params schema should be declared exactly once. Got:\n{}",
types_ts
);
}