use super::{HookFilter, HookFilterPlan};
fn plan(json: &str) -> HookFilterPlan {
let v: serde_json::Value = serde_json::from_str(json).unwrap();
HookFilterPlan::from_json(Some(&v))
}
#[test]
fn absent_hook_wants_nothing() {
let p = plan(r#"{"present":false,"unfiltered":false,"plugins":[]}"#);
assert!(!p.wants("/app/src/main.tsx", None));
}
#[test]
fn unfiltered_hook_wants_everything() {
let p = plan(r#"{"present":true,"unfiltered":true,"plugins":[]}"#);
assert!(p.wants("anything", None));
}
#[test]
fn id_filter_gates_by_module_id() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[{"source":"\\.tsx$","flags":""}],"code":[]}]}"#,
);
assert!(p.wants("/app/src/App.tsx", None));
assert!(!p.wants("/app/node_modules/react/index.js", None));
}
#[test]
fn id_and_code_filters_of_one_plugin_both_apply() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[{"source":"\\.js$","flags":""}],"code":[{"literal":"import.meta.glob"}]}]}"#,
);
assert!(p.wants("/a.js", Some("import.meta.glob(\"./x\")")));
assert!(!p.wants("/a.js", Some("plain code")));
assert!(!p.wants("/a.ts", Some("import.meta.glob(\"./x\")")));
assert!(p.wants("/a.js", None));
}
#[test]
fn filters_union_across_plugins() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[{"source":"\\.md$","flags":""}],"code":[]},{"id":[{"source":"\\.svg$","flags":""}],"code":[]}]}"#,
);
assert!(p.wants("/doc.md", None));
assert!(p.wants("/icon.svg", None));
assert!(!p.wants("/main.ts", None));
}
#[test]
fn case_insensitive_js_regex_carries_over() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[{"source":"\\.SVG$","flags":"i"}],"code":[]}]}"#,
);
assert!(p.wants("/icon.svg", None));
}
#[test]
fn dotall_carries_and_multiline_fails_open() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[],"code":[{"source":"a.b","flags":"s"}]}]}"#,
);
assert!(p.wants("/a.js", Some("a\nb")));
assert!(!p.wants("/a.js", Some("ab")));
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[],"code":[{"source":"^import\\s","flags":"m"}]}]}"#,
);
assert!(p.wants("/a.js", Some("// banner\rimport x from \"y\";")));
}
#[test]
fn windows_ids_match_slash_normalized_like_the_host() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[{"source":"/src/.*\\.tsx$","flags":""}],"code":[]}]}"#,
);
assert!(p.wants(r"C:\app\src\App.tsx", None));
assert!(!p.wants(r"C:\app\node_modules\x\index.js", None));
}
#[test]
fn uncompilable_regex_fails_open_to_unfiltered() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[{"source":"(?!never)x","flags":""}],"code":[]}]}"#,
);
assert!(p.wants("/anything/at/all", None));
}
#[test]
fn malformed_plan_fails_open() {
let p = HookFilterPlan::from_json(None);
assert!(p.wants("x", None));
let p = plan(r#"{"plugins":"nope"}"#);
assert!(p.wants("x", None));
}
#[test]
fn empty_filter_entry_fails_open() {
let p = plan(r#"{"present":true,"unfiltered":false,"plugins":[{"id":[],"code":[]}]}"#);
assert!(p.wants("/anything", None));
}
#[test]
fn string_id_globs_gate_like_vites_picomatch() {
let p = plan(
r#"{"present":true,"unfiltered":false,"plugins":[{"id":[{"glob":"**/*.svg"},{"glob":"/app/src/*.{md,mdx}"}],"code":[]}]}"#,
);
assert!(p.wants("/app/src/icon.svg", None));
assert!(p.wants("/app/src/.hidden/icon.svg", None), "dot: true");
assert!(
p.wants("/app/node_modules/x/icon.svg", None),
"no node_modules ignore"
);
assert!(p.wants("/app/src/doc.mdx", None));
assert!(!p.wants("/app/src/deep/doc.md", None));
assert!(!p.wants("/app/src/main.ts", None));
}
#[test]
fn extglobs_and_unknown_patterns_fail_open() {
for item in [
r#"{"glob":"**/*.@(svg|png)"}"#,
r#"{"glob":"/app/src/[[:alpha:]].ts"}"#,
r#"{"glob":"/app/src/*.{1..3}"}"#,
r#"{"glob":"**/*.[!j]s"}"#,
r#"{"glob":"!**/*.ts"}"#,
r#"{"any":true}"#,
r#"{"source":"(?<=x)y","flags":""}"#,
] {
let f = HookFilter::from_json(&serde_json::from_str(item).unwrap());
assert!(f.is_match("/anything.ts"), "{item} matches everything");
}
}
#[test]
fn ascii_word_boundaries_match_like_js() {
let filters = HookFilter::list_from_json(r#"[{"source":"\\bimport\\b","flags":""}]"#);
assert!(filters[0].is_match("éimport x"));
assert!(!filters[0].is_match("reimported"));
}
#[test]
fn rust_only_regex_syntax_reads_as_js_does() {
use super::rust_regex_from_js as js;
let re = |source: &str| regex::Regex::new(&js(source, "").unwrap()).unwrap();
assert!(re("[a&&b]").is_match("&"));
assert!(re("[a~~b]").is_match("~"));
assert!(re("[[x]").is_match("["));
assert!(re(r"\pL").is_match("pL"));
assert!(!re(r"\pL").is_match("é"));
assert!(re(r"\Afoo\z").is_match("xAfooz"));
assert!(!re(r"^\d$").is_match("\u{0663}"));
assert!(!re(r"^[\w]$").is_match("é"));
assert!(re(r"^\s$").is_match("\u{FEFF}"));
assert!(re(r"^\W$").is_match("é"));
assert_eq!(js(r"\.svg$", "i").as_deref(), Some(r"(?i)\.svg$"));
}
#[test]
fn js_regex_syntax_rust_cannot_carry_fails_open() {
use super::rust_regex_from_js as js;
for (source, flags) in [
("x", "m"),
("x", "u"),
("[+--]", ""),
(r"[\W]", ""),
(r"\x{41}", ""),
(r"\u{41}", ""),
] {
assert_eq!(js(source, flags), None, "/{source}/{flags}");
}
let f = HookFilter::from_json(&serde_json::json!({"source": "(?<=x)y", "flags": ""}));
assert!(
f.is_match("/anything.ts"),
"an uncompilable regex matches everything"
);
}