#![cfg(feature = "swc")]
use super::applicator::PatchApplicator;
use super::collector::PatchCollector;
use super::helpers::{dedupe_imports, dedupe_patches, parse_import_patch};
use crate::ts_syn::abi::{Patch, PatchCode, SpanIR};
#[test]
fn test_insert_patch() {
let source = "class Foo {}";
let patch = Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar: string; ".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert_eq!(result, "class Foo { bar: string; }");
}
#[test]
fn test_replace_patch() {
let source = "class Foo { old: number; }";
let patch = Patch::Replace {
span: SpanIR { start: 13, end: 26 },
code: "new: string;".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert_eq!(result, "class Foo { new: string;}");
}
#[test]
fn test_delete_patch() {
let source = "class Foo { unnecessary: any; }";
let patch = Patch::Delete {
span: SpanIR { start: 13, end: 31 },
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert_eq!(result, "class Foo { }");
}
#[test]
fn test_multiple_patches() {
let source = "class Foo {}";
let patches = vec![
Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar: string;".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " baz: number;".to_string().into(),
source_macro: None,
},
];
let applicator = PatchApplicator::new(source, patches);
let result = applicator.apply().unwrap();
assert!(result.contains("bar: string"));
assert!(result.contains("baz: number"));
}
#[test]
fn test_replace_multiline_block_with_single_line() {
let source = "class C { constructor() { /* body */ } }";
let constructor_start = source.find("constructor").unwrap();
let constructor_end = source.find("} }").unwrap() + 1;
let patch = Patch::Replace {
span: SpanIR {
start: constructor_start as u32 + 1,
end: constructor_end as u32 + 1,
},
code: "constructor();".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
let expected = "class C { constructor(); }";
assert_eq!(result, expected);
}
#[test]
fn test_detect_indentation_spaces() {
let source = r#"class User {
id: number;
name: string;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let applicator = PatchApplicator::new(source, vec![]);
let indent = applicator.detect_indentation(closing_brace_pos);
assert_eq!(indent, " ");
}
#[test]
fn test_detect_indentation_tabs() {
let source = "class User {\n\tid: number;\n}";
let closing_brace_pos = source.rfind('}').unwrap();
let applicator = PatchApplicator::new(source, vec![]);
let indent = applicator.detect_indentation(closing_brace_pos);
assert_eq!(indent, "\t");
}
#[test]
fn test_format_insertion_adds_newline_and_indent() {
let source = r#"class User {
id: number;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let applicator = PatchApplicator::new(source, vec![]);
use swc_core::ecma::ast::{ClassMember, EmptyStmt};
let code = PatchCode::ClassMember(ClassMember::Empty(EmptyStmt {
span: swc_core::common::DUMMY_SP,
}));
let formatted = applicator.format_insertion("toString(): string;", closing_brace_pos, &code);
assert!(formatted.starts_with('\n'));
assert!(formatted.contains("toString(): string;"));
}
#[test]
fn test_insert_class_member_with_proper_formatting() {
let source = r#"class User {
id: number;
name: string;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let patch = Patch::Insert {
at: SpanIR {
start: closing_brace_pos as u32 + 1,
end: closing_brace_pos as u32 + 1,
},
code: "toString(): string;".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert!(result.contains("toString(): string;"));
}
#[test]
fn test_multiple_class_member_insertions() {
let source = r#"class User {
id: number;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let patches = vec![
Patch::Insert {
at: SpanIR {
start: closing_brace_pos as u32 + 1,
end: closing_brace_pos as u32 + 1,
},
code: "toString(): string;".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR {
start: closing_brace_pos as u32 + 1,
end: closing_brace_pos as u32 + 1,
},
code: "toJSON(): Record<string, unknown>;".to_string().into(),
source_macro: None,
},
];
let applicator = PatchApplicator::new(source, patches);
let result = applicator.apply().unwrap();
assert!(result.contains("toString(): string;"));
assert!(result.contains("toJSON(): Record<string, unknown>;"));
}
#[test]
fn test_indentation_preserved_in_nested_class() {
let source = r#"export namespace Models {
class User {
id: number;
}
}"#;
let closing_brace_pos = source.find(" }").unwrap() + 2; let applicator = PatchApplicator::new(source, vec![]);
let indent = applicator.detect_indentation(closing_brace_pos);
assert_eq!(indent, " ");
}
#[test]
fn test_no_formatting_for_text_patches() {
let source = "class User {}";
let pos = 11; let applicator = PatchApplicator::new(source, vec![]);
let formatted = applicator.format_insertion("test", pos, &PatchCode::Text("test".to_string()));
assert_eq!(formatted, "test");
}
#[test]
fn test_dedupe_patches_removes_identical_inserts() {
let mut patches = vec![
Patch::Insert {
at: SpanIR { start: 11, end: 11 },
code: "console.log('a');".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR { start: 11, end: 11 },
code: "console.log('a');".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR { start: 21, end: 21 },
code: "console.log('b');".to_string().into(),
source_macro: None,
},
];
dedupe_patches(&mut patches).expect("dedupe should succeed");
assert_eq!(
patches.len(),
2,
"duplicate inserts should collapse to a single patch"
);
assert!(
patches
.iter()
.any(|patch| matches!(patch, Patch::Insert { at, .. } if at.start == 21)),
"dedupe should retain distinct spans"
);
}
#[test]
fn test_apply_with_mapping_no_patches() {
let source = "class Foo {}";
let applicator = PatchApplicator::new(source, vec![]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, source);
assert_eq!(result.mapping.segments.len(), 1);
assert!(result.mapping.generated_regions.is_empty());
assert_eq!(result.mapping.original_to_expanded(0), 0);
assert_eq!(result.mapping.original_to_expanded(5), 5);
assert_eq!(result.mapping.expanded_to_original(5), Some(5));
}
#[test]
fn test_apply_with_mapping_simple_insert() {
let source = "class Foo {}";
let patch = Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar;".to_string().into(),
source_macro: Some("Test".to_string()),
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "class Foo { bar;}");
assert_eq!(result.code.len(), 17);
assert_eq!(result.mapping.segments.len(), 2);
assert_eq!(result.mapping.generated_regions.len(), 1);
let seg1 = &result.mapping.segments[0];
assert_eq!(seg1.original_start, 0);
assert_eq!(seg1.original_end, 11);
assert_eq!(seg1.expanded_start, 0);
assert_eq!(seg1.expanded_end, 11);
let generated = &result.mapping.generated_regions[0];
assert_eq!(generated.start, 11);
assert_eq!(generated.end, 16);
assert_eq!(generated.source_macro, "Test");
let seg2 = &result.mapping.segments[1];
assert_eq!(seg2.original_start, 11);
assert_eq!(seg2.original_end, 12);
assert_eq!(seg2.expanded_start, 16);
assert_eq!(seg2.expanded_end, 17);
assert_eq!(result.mapping.original_to_expanded(0), 0);
assert_eq!(result.mapping.original_to_expanded(10), 10);
assert_eq!(result.mapping.original_to_expanded(11), 16);
assert_eq!(result.mapping.expanded_to_original(5), Some(5));
assert_eq!(result.mapping.expanded_to_original(12), None); assert_eq!(result.mapping.expanded_to_original(16), Some(11));
}
#[test]
fn test_apply_with_mapping_replace() {
let source = "let x = old;";
let patch = Patch::Replace {
span: SpanIR { start: 9, end: 12 },
code: "new".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "let x = new;");
assert_eq!(result.mapping.segments.len(), 2);
assert_eq!(result.mapping.generated_regions.len(), 1);
let seg1 = &result.mapping.segments[0];
assert_eq!(seg1.original_start, 0);
assert_eq!(seg1.original_end, 8);
let generated = &result.mapping.generated_regions[0];
assert_eq!(generated.start, 8);
assert_eq!(generated.end, 11);
let seg2 = &result.mapping.segments[1];
assert_eq!(seg2.original_start, 11);
assert_eq!(seg2.original_end, 12);
assert_eq!(seg2.expanded_start, 11);
assert_eq!(seg2.expanded_end, 12);
assert_eq!(result.mapping.expanded_to_original(9), None);
}
#[test]
fn test_apply_with_mapping_delete() {
let source = "let x = 1; let y = 2;";
let patch = Patch::Delete {
span: SpanIR { start: 11, end: 21 },
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "let x = 1;;");
assert_eq!(result.mapping.segments.len(), 2);
assert_eq!(result.mapping.generated_regions.len(), 0);
assert_eq!(result.mapping.original_to_expanded(20), 10);
assert_eq!(result.mapping.expanded_to_original(10), Some(20));
}
#[test]
fn test_apply_with_mapping_multiple_inserts() {
let source = "a;b;c;";
let patches = vec![
Patch::Insert {
at: SpanIR { start: 3, end: 3 },
code: "X".to_string().into(),
source_macro: Some("multi".to_string()),
},
Patch::Insert {
at: SpanIR { start: 5, end: 5 },
code: "Y".to_string().into(),
source_macro: Some("multi".to_string()),
},
];
let applicator = PatchApplicator::new(source, patches);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "a;Xb;Yc;");
assert_eq!(result.mapping.segments.len(), 3);
assert_eq!(result.mapping.generated_regions.len(), 2);
assert_eq!(result.mapping.original_to_expanded(0), 0); assert_eq!(result.mapping.original_to_expanded(2), 3); assert_eq!(result.mapping.original_to_expanded(4), 6);
assert!(result.mapping.is_in_generated(2)); assert!(result.mapping.is_in_generated(5)); assert!(!result.mapping.is_in_generated(0)); assert!(!result.mapping.is_in_generated(3)); }
#[test]
fn test_apply_with_mapping_span_mapping() {
let source = "class Foo {}";
let patch = Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar();".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
let (exp_start, exp_len) = result.mapping.map_span_to_expanded(0, 5);
assert_eq!(exp_start, 0);
assert_eq!(exp_len, 5);
let orig = result.mapping.map_span_to_original(0, 5);
assert_eq!(orig, Some((0, 5)));
let gen_span = result.mapping.map_span_to_original(12, 3);
assert_eq!(gen_span, None);
}
#[test]
fn test_patch_collector_with_mapping() {
let source = "class Foo {}";
let mut collector = PatchCollector::new();
collector.add_runtime_patches(vec![Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " toString() {}".to_string().into(),
source_macro: Some("Debug".to_string()),
}]);
let result = collector
.apply_runtime_patches_with_mapping(source, None)
.unwrap();
assert!(result.code.contains("toString()"));
assert_eq!(result.mapping.generated_regions.len(), 1);
assert_eq!(result.mapping.generated_regions[0].source_macro, "Debug");
}
#[test]
fn test_parse_import_patch_value() {
let code = "import { Option } from \"effect\";\n";
let result = parse_import_patch(code);
assert_eq!(
result,
Some(("Option".to_string(), "effect".to_string(), false))
);
}
#[test]
fn test_parse_import_patch_type_only() {
let code = "import type { Exit } from \"effect\";\n";
let result = parse_import_patch(code);
assert_eq!(
result,
Some(("Exit".to_string(), "effect".to_string(), true))
);
}
#[test]
fn test_parse_import_patch_aliased() {
let code = "import { Option as __gigaform_reexport_Option } from \"effect\";\n";
let result = parse_import_patch(code);
assert_eq!(
result,
Some(("Option".to_string(), "effect".to_string(), false))
);
}
#[test]
fn test_dedupe_imports_value_subsumes_type() {
let mut patches = vec![
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import type { Exit } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import { Exit } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
];
dedupe_imports(&mut patches);
assert_eq!(
patches.len(),
1,
"type-only import should be removed when value import exists"
);
assert!(patches[0].source_macro().is_none());
if let Patch::InsertRaw { code, .. } = &patches[0] {
assert!(
!code.contains("import type"),
"remaining import should be the value import"
);
assert!(code.contains("import { Exit }"));
} else {
panic!("expected InsertRaw");
}
}
#[test]
fn test_dedupe_imports_keeps_unrelated() {
let mut patches = vec![
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import type { FieldController } from \"$lib/types/gigaform\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import { Option } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
];
dedupe_imports(&mut patches);
assert_eq!(patches.len(), 2, "unrelated imports should both be kept");
}
#[test]
fn test_dedupe_imports_different_modules_kept() {
let mut patches = vec![
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import type { Option } from \"effect/Option\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import { Option } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
];
dedupe_imports(&mut patches);
assert_eq!(
patches.len(),
2,
"imports from different modules should both be kept"
);
}