use oxc_coverage_instrument::{InstrumentOptions, instrument};
fn default_opts() -> InstrumentOptions {
InstrumentOptions::default()
}
fn instrument_js(source: &str) -> oxc_coverage_instrument::InstrumentResult {
instrument(source, "test.js", &default_opts()).unwrap()
}
#[test]
fn statement_simple_variable_declaration() {
let result = instrument_js("const x = 1;");
assert_eq!(result.coverage_map.statement_map.len(), 1);
assert!(result.code.contains("++") && result.code.contains(".s[0]"));
}
#[test]
fn statement_multiple_statements() {
let result = instrument_js("const x = 1;\nconst y = 2;\nconst z = x + y;");
assert_eq!(result.coverage_map.statement_map.len(), 3);
}
#[test]
fn statement_return_throw_expression() {
let result = instrument_js(
"function f() { const x = 1; return x; }\nfunction g() { throw new Error(); }",
);
assert!(result.coverage_map.statement_map.len() >= 4);
}
#[test]
fn statement_empty_and_block_not_counted() {
let result = instrument_js(";;; { const x = 1; }");
assert_eq!(result.coverage_map.statement_map.len(), 1);
}
#[test]
fn function_declaration() {
let result = instrument_js("function add(a, b) { return a + b; }");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "add");
assert!(result.code.contains(".f[0]"));
}
#[test]
fn function_expression() {
let result = instrument_js("const add = function(a, b) { return a + b; };");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "add");
}
#[test]
fn arrow_function_expression_body() {
let result = instrument_js("const double = (x) => x * 2;");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "double");
assert!(result.code.contains("return"));
assert!(result.code.contains(".f[0]"));
}
#[test]
fn arrow_function_block_body() {
let result = instrument_js("const add = (a, b) => { return a + b; };");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "add");
}
#[test]
fn class_method() {
let result =
instrument_js("class Calc { add(a, b) { return a + b; } sub(a, b) { return a - b; } }");
assert_eq!(result.coverage_map.fn_map.len(), 2);
assert_eq!(result.coverage_map.fn_map["0"].name, "add");
assert_eq!(result.coverage_map.fn_map["1"].name, "sub");
}
#[test]
fn anonymous_function() {
let result = instrument_js("setTimeout(function() { console.log('hi'); }, 100);");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert!(result.coverage_map.fn_map["0"].name.starts_with("(anonymous_"));
}
#[test]
fn multiple_functions() {
let result = instrument_js(
"function a() {} function b() {} const c = () => 1; const d = function() {};",
);
assert_eq!(result.coverage_map.fn_map.len(), 4);
}
#[test]
fn branch_if_else() {
let result = instrument_js("if (true) { console.log('yes'); } else { console.log('no'); }");
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert_eq!(result.coverage_map.branch_map["0"].branch_type, "if");
assert_eq!(result.coverage_map.branch_map["0"].locations.len(), 2);
assert!(result.code.contains(".b[0][0]"));
assert!(result.code.contains(".b[0][1]"));
}
#[test]
fn branch_if_without_else() {
let result = instrument_js("if (true) { console.log('yes'); }");
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert_eq!(result.coverage_map.branch_map["0"].locations.len(), 2);
assert!(result.code.contains(".b[0][0]"));
}
#[test]
fn branch_ternary() {
let result = instrument_js("const x = true ? 1 : 0;");
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert_eq!(result.coverage_map.branch_map["0"].branch_type, "cond-expr");
assert!(result.code.contains(".b[0][0]"));
assert!(result.code.contains(".b[0][1]"));
}
#[test]
fn branch_switch() {
let result = instrument_js(
"switch(x) { case 1: console.log('one'); break; case 2: console.log('two'); break; default: console.log('other'); }",
);
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert_eq!(result.coverage_map.branch_map["0"].branch_type, "switch");
assert_eq!(result.coverage_map.branch_map["0"].locations.len(), 3);
}
#[test]
fn branch_logical_and() {
let result = instrument_js("const x = a && b;");
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert_eq!(result.coverage_map.branch_map["0"].branch_type, "binary-expr");
}
#[test]
fn branch_logical_or() {
let result = instrument_js("const x = a || b;");
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert_eq!(result.coverage_map.branch_map["0"].branch_type, "binary-expr");
}
#[test]
fn branch_nullish_coalescing() {
let result = instrument_js("const x = a ?? b;");
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert_eq!(result.coverage_map.branch_map["0"].branch_type, "binary-expr");
assert!(result.code.contains(".b[0][0]"));
assert!(result.code.contains(".b[0][1]"));
}
#[test]
fn branch_nullish_assignment() {
let result = instrument_js("let x = null; x ??= 42;");
let binary_branches: Vec<_> = result
.coverage_map
.branch_map
.values()
.filter(|b| b.branch_type == "binary-expr")
.collect();
assert_eq!(binary_branches.len(), 1);
assert_eq!(binary_branches[0].locations.len(), 2);
}
#[test]
fn branch_logical_or_assignment() {
let result = instrument_js("let x = 0; x ||= 'default';");
let binary_branch_count =
result.coverage_map.branch_map.values().filter(|b| b.branch_type == "binary-expr").count();
assert_eq!(binary_branch_count, 1);
}
#[test]
fn branch_logical_and_assignment() {
let result = instrument_js("let x = 1; x &&= doSomething();");
let binary_branch_count =
result.coverage_map.branch_map.values().filter(|b| b.branch_type == "binary-expr").count();
assert_eq!(binary_branch_count, 1);
}
#[test]
fn loops_do_not_create_branch_entries() {
let result = instrument_js(
"for (let i = 0; i < 10; i++) { x(); } for (const k in o) { y(); } for (const v of a) { z(); } while (true) { break; } do { w(); } while (false);",
);
assert!(
result.coverage_map.branch_map.is_empty(),
"Loops should not produce branch entries (matching Istanbul)"
);
}
#[test]
fn pragma_istanbul_ignore_file() {
let result = instrument_js("/* istanbul ignore file */\nfunction f() { return 1; }");
assert!(result.coverage_map.fn_map.is_empty());
assert!(result.coverage_map.statement_map.is_empty());
assert!(result.coverage_map.branch_map.is_empty());
assert!(!result.code.contains("cov_"));
}
#[test]
fn pragma_v8_ignore_file() {
let result = instrument_js("/* v8 ignore file */\nfunction f() { return 1; }");
assert!(result.coverage_map.fn_map.is_empty());
}
#[test]
fn pragma_istanbul_ignore_next_function() {
let result = instrument_js(
"/* istanbul ignore next */\nfunction ignored() { return 1; }\nfunction counted() { return 2; }",
);
let fn_names: Vec<&str> =
result.coverage_map.fn_map.values().map(|f| f.name.as_str()).collect();
assert!(fn_names.contains(&"counted"));
assert!(!fn_names.contains(&"ignored"));
}
#[test]
fn source_map_generation() {
let opts = InstrumentOptions { source_map: true, ..InstrumentOptions::default() };
let result = instrument("function f() { return 1; }", "test.js", &opts).unwrap();
assert!(result.source_map.is_some());
let sm = result.source_map.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&sm).unwrap();
assert_eq!(parsed["version"], 3);
}
#[test]
fn source_map_disabled_by_default() {
let result = instrument_js("function f() { return 1; }");
assert!(result.source_map.is_none());
}
#[test]
fn source_map_accounts_for_preamble_offset() {
let source = "function f() {\n return 1;\n}";
let opts = InstrumentOptions { source_map: true, ..InstrumentOptions::default() };
let result = instrument(source, "test.js", &opts).unwrap();
let sm_json = result.source_map.as_ref().unwrap();
let sm = oxc_sourcemap::SourceMap::from_json_string(sm_json).unwrap();
let tokens: Vec<_> = sm.get_tokens().collect();
assert!(!tokens.is_empty(), "Source map should have at least one mapping");
let first_gen_line = tokens[0].get_dst_line();
assert!(
first_gen_line >= 1,
"First mapping should be on line >= 1 (after preamble), got line {first_gen_line}"
);
}
#[test]
fn parse_error_returns_err() {
let result = instrument("function {{{", "bad.js", &default_opts());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("parse error"));
}
#[test]
fn coverage_map_has_required_fields() {
let result = instrument_js("function f() { return 1; }");
let json = serde_json::to_value(&result.coverage_map).unwrap();
assert!(json["path"].is_string());
assert!(json["statementMap"].is_object());
assert!(json["fnMap"].is_object());
assert!(json["branchMap"].is_object());
assert!(json["s"].is_object());
assert!(json["f"].is_object());
assert!(json["b"].is_object());
}
#[test]
fn hit_counts_initialized_to_zero() {
let result = instrument_js("function f() { return 1; }");
for count in result.coverage_map.s.values() {
assert_eq!(*count, 0);
}
for count in result.coverage_map.f.values() {
assert_eq!(*count, 0);
}
for counts in result.coverage_map.b.values() {
for count in counts {
assert_eq!(*count, 0);
}
}
}
#[test]
fn statement_map_keys_are_sequential_strings() {
let result = instrument_js("const a = 1;\nconst b = 2;\nconst c = 3;");
let keys: Vec<usize> =
result.coverage_map.statement_map.keys().map(|k| k.parse::<usize>().unwrap()).collect();
let mut sorted = keys.clone();
sorted.sort_unstable();
assert_eq!(keys, sorted);
assert_eq!(sorted, vec![0, 1, 2]);
}
#[test]
fn positions_are_1_based_line_0_based_column() {
let result = instrument_js("const x = 1;");
let loc = &result.coverage_map.statement_map["0"];
assert!(loc.start.line >= 1, "Line should be 1-based");
}
#[test]
fn custom_coverage_variable() {
let opts = InstrumentOptions {
coverage_variable: "__custom_cov__".to_string(),
..InstrumentOptions::default()
};
let result = instrument("const x = 1;", "test.js", &opts).unwrap();
assert!(result.code.contains("__custom_cov__"));
}
#[test]
fn deterministic_cov_function_name() {
let result1 = instrument_js("const x = 1;");
let result2 = instrument_js("const x = 1;");
let extract_name = |code: &str| -> String {
let start = code.find("var cov_").unwrap() + 4;
let end = code[start..].find(' ').unwrap() + start;
code[start..end].to_string()
};
assert_eq!(extract_name(&result1.code), extract_name(&result2.code));
}
#[test]
fn empty_source() {
let result = instrument_js("");
assert!(result.coverage_map.fn_map.is_empty());
assert!(result.coverage_map.statement_map.is_empty());
assert!(result.coverage_map.branch_map.is_empty());
}
#[test]
fn nested_functions() {
let result =
instrument_js("function outer() { function inner() { return 1; } return inner(); }");
assert_eq!(result.coverage_map.fn_map.len(), 2);
}
#[test]
fn nested_if_else() {
let result = instrument_js("if (a) { if (b) { x(); } else { y(); } } else { z(); }");
let if_branch_count =
result.coverage_map.branch_map.values().filter(|b| b.branch_type == "if").count();
assert_eq!(if_branch_count, 2);
}
#[test]
fn chained_logical_expressions() {
let result = instrument_js("const x = a && b && c;");
let binary_branches: Vec<_> = result
.coverage_map
.branch_map
.values()
.filter(|b| b.branch_type == "binary-expr")
.collect();
assert_eq!(binary_branches.len(), 1);
assert_eq!(binary_branches[0].locations.len(), 3);
}
#[test]
fn typescript_source() {
let opts = InstrumentOptions::default();
let result = instrument(
"function add(a: number, b: number): number { return a + b; }",
"test.ts",
&opts,
)
.unwrap();
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "add");
}
#[test]
fn jsx_source() {
let opts = InstrumentOptions::default();
let result =
instrument("function App() { return <div>Hello</div>; }", "test.jsx", &opts).unwrap();
assert_eq!(result.coverage_map.fn_map.len(), 1);
}
#[test]
fn coverage_map_json_roundtrip() {
let result = instrument_js("function f() { if (true) { return 1; } return 0; }");
let json = serde_json::to_string(&result.coverage_map).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.is_object());
assert_eq!(parsed["path"], "test.js");
}
#[test]
fn nested_arrow_functions_both_get_counters() {
let result = instrument_js("const f = (x) => (y) => x + y;");
assert_eq!(result.coverage_map.fn_map.len(), 2);
assert_eq!(result.coverage_map.f.len(), 2);
assert!(result.code.contains(".f[0]"));
assert!(result.code.contains(".f[1]"));
}
#[test]
fn deeply_nested_arrows() {
let result = instrument_js("const f = (a) => (b) => (c) => a + b + c;");
assert_eq!(result.coverage_map.fn_map.len(), 3);
assert_eq!(result.coverage_map.f.len(), 3);
}
#[test]
fn pragma_ignore_next_arrow_function() {
let result = instrument_js(
"/* istanbul ignore next */\nconst ignored = () => 1;\nconst counted = () => 2;",
);
let fn_names: Vec<&str> =
result.coverage_map.fn_map.values().map(|f| f.name.as_str()).collect();
assert!(fn_names.contains(&"counted"));
assert!(!fn_names.contains(&"ignored"));
}
#[test]
fn pragma_ignore_if_skips_consequent_counter() {
let result = instrument_js(
"function f(x) {\n /* istanbul ignore if */\n if (x < 0) { throw new Error(); }\n return x;\n}",
);
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert!(!result.code.contains(".b[0][0]"));
}
#[test]
fn pragma_ignore_else_skips_alternate_counter() {
let result = instrument_js(
"function f(x) {\n /* istanbul ignore else */\n if (x > 0) { return 'pos'; } else { return 'neg'; }\n}",
);
assert_eq!(result.coverage_map.branch_map.len(), 1);
assert!(result.code.contains(".b[0][0]"));
assert!(!result.code.contains(".b[0][1]"));
}
#[test]
fn unknown_pragma_populates_unhandled_pragmas() {
let result = instrument_js("/* istanbul ignore banana */\nfunction f() { return 1; }");
assert!(!result.unhandled_pragmas.is_empty());
assert!(result.unhandled_pragmas[0].comment.contains("banana"));
assert_eq!(result.unhandled_pragmas[0].line, 1);
}
#[test]
fn known_pragmas_not_in_unhandled() {
let result = instrument_js("/* istanbul ignore next */\nfunction f() { return 1; }");
assert!(result.unhandled_pragmas.is_empty());
}
#[test]
fn pragma_v8_ignore_next() {
let result =
instrument_js("/* v8 ignore next */\nfunction ignored() {}\nfunction counted() {}");
let fn_names: Vec<&str> =
result.coverage_map.fn_map.values().map(|f| f.name.as_str()).collect();
assert!(!fn_names.contains(&"ignored"));
assert!(fn_names.contains(&"counted"));
}
#[test]
fn pragma_c8_ignore_file() {
let result = instrument_js("/* c8 ignore file */\nfunction f() { return 1; }");
assert!(result.coverage_map.fn_map.is_empty());
}
#[test]
fn input_source_map_stored_on_coverage() {
let opts = InstrumentOptions {
input_source_map: Some(
r#"{"version":3,"sources":["test.ts"],"mappings":"AAAA"}"#.to_string(),
),
..InstrumentOptions::default()
};
let result = instrument("const x = 1;", "test.js", &opts).unwrap();
let json = serde_json::to_value(&result.coverage_map).unwrap();
assert!(json["inputSourceMap"].is_object());
assert_eq!(json["inputSourceMap"]["version"], 3);
}
#[test]
fn input_source_map_none_by_default() {
let result = instrument_js("const x = 1;");
let json = serde_json::to_value(&result.coverage_map).unwrap();
assert!(json.get("inputSourceMap").is_none());
}
#[test]
fn source_map_composed_with_input_source_map() {
let opts = InstrumentOptions {
source_map: true,
input_source_map: Some(
r#"{"version":3,"sources":["original.ts"],"sourcesContent":["const x: number = 1;"],"mappings":"AAAA"}"#.to_string(),
),
..InstrumentOptions::default()
};
let result = instrument("const x = 1;", "test.js", &opts).unwrap();
assert!(result.source_map.is_some());
let sm: serde_json::Value = serde_json::from_str(result.source_map.as_ref().unwrap()).unwrap();
let sources = sm["sources"].as_array().unwrap();
let has_original = sources.iter().any(|s| s.as_str() == Some("original.ts"));
assert!(has_original, "Composed source map should reference original.ts, got: {sources:?}");
}
#[test]
fn input_source_map_invalid_json_ignored() {
let opts = InstrumentOptions {
input_source_map: Some("not valid json".to_string()),
..InstrumentOptions::default()
};
let result = instrument("const x = 1;", "test.js", &opts).unwrap();
let json = serde_json::to_value(&result.coverage_map).unwrap();
assert!(json.get("inputSourceMap").is_none());
}
#[test]
fn invalid_coverage_variable_returns_error() {
let opts = InstrumentOptions {
coverage_variable: "it's_broken".to_string(),
..InstrumentOptions::default()
};
let result = instrument("const x = 1;", "test.js", &opts);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid coverage variable"));
}
#[test]
fn valid_coverage_variable_with_dollar() {
let opts = InstrumentOptions {
coverage_variable: "$coverage".to_string(),
..InstrumentOptions::default()
};
let result = instrument("const x = 1;", "test.js", &opts);
assert!(result.is_ok());
assert!(result.unwrap().code.contains("$coverage"));
}
#[test]
fn async_function_declaration() {
let result = instrument_js("async function fetchData() { return await fetch('/api'); }");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "fetchData");
let decl = &result.coverage_map.fn_map["0"].decl;
assert!(decl.end.column > 8, "decl_span should extend past 'function' for async");
}
#[test]
fn async_arrow_function() {
let result = instrument_js("const f = async (x) => { return await x; };");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "f");
}
#[test]
fn destructuring_default_creates_branch() {
let result = instrument_js("const { x = 1, y = 2 } = obj;");
let default_count =
result.coverage_map.branch_map.values().filter(|b| b.branch_type == "default-arg").count();
assert_eq!(default_count, 2);
}
#[test]
fn computed_method_key_uses_anonymous_name() {
let result = instrument_js("class C { [Symbol.iterator]() { return this; } }");
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert!(result.coverage_map.fn_map["0"].name.contains("anonymous"));
}
#[test]
fn switch_fall_through_cases() {
let result = instrument_js(
"function f(x) { switch(x) { case 1: case 2: return 'a'; case 3: return 'b'; } }",
);
let switch_branches: Vec<_> =
result.coverage_map.branch_map.values().filter(|b| b.branch_type == "switch").collect();
assert_eq!(switch_branches.len(), 1);
assert_eq!(switch_branches[0].locations.len(), 3);
}
#[test]
fn unknown_extension_treated_as_js() {
let result = instrument("function f() { return 1; }", "test.coffee", &default_opts());
assert!(result.is_ok());
assert_eq!(result.unwrap().coverage_map.fn_map.len(), 1);
}
#[test]
fn source_map_with_ignore_file() {
let opts = InstrumentOptions { source_map: true, ..InstrumentOptions::default() };
let result =
instrument("/* istanbul ignore file */\nfunction f() { return 1; }", "test.js", &opts)
.unwrap();
assert!(result.source_map.is_none());
}
#[test]
fn multiple_parse_errors_joined() {
let result = instrument("function { const }", "bad.js", &default_opts());
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("parse error"));
}
#[test]
fn parse_coverage_map_roundtrip() {
use oxc_coverage_instrument::parse_coverage_map;
let result = instrument_js("function f() { if (true) { return 1; } return 0; }");
let mut root = std::collections::BTreeMap::new();
root.insert(result.coverage_map.path.clone(), &result.coverage_map);
let json = serde_json::to_string(&root).unwrap();
let parsed = parse_coverage_map(&json).unwrap();
assert!(parsed.contains_key("test.js"));
assert_eq!(parsed["test.js"].fn_map.len(), result.coverage_map.fn_map.len());
}
#[test]
fn file_coverage_from_json_roundtrip() {
use oxc_coverage_instrument::FileCoverage;
let result = instrument_js("function f() { return 1; }");
let json = serde_json::to_string(&result.coverage_map).unwrap();
let parsed = FileCoverage::from_json(&json).unwrap();
assert_eq!(parsed.path, "test.js");
assert_eq!(parsed.fn_map.len(), result.coverage_map.fn_map.len());
}
#[test]
fn parse_coverage_map_invalid_json() {
use oxc_coverage_instrument::parse_coverage_map;
assert!(parse_coverage_map("not json").is_err());
}
#[test]
fn parse_coverage_map_null_hit_counts() {
use oxc_coverage_instrument::parse_coverage_map;
let json = r#"{
"test.js": {
"path": "test.js",
"statementMap": {"0": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": null}}},
"fnMap": {"0": {"name": "f", "line": null, "decl": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": null}}, "loc": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": null}}}},
"branchMap": {"0": {"loc": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": null}}, "line": 1, "type": "if", "locations": [{"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": null}}, {"start": {}, "end": {}}]}},
"s": {"0": null},
"f": {"0": null},
"b": {"0": [null, 1]}
}
}"#;
let parsed = parse_coverage_map(json).unwrap();
let file = &parsed["test.js"];
assert_eq!(file.s["0"], 0, "null statement count should coerce to 0");
assert_eq!(file.f["0"], 0, "null function count should coerce to 0");
assert_eq!(file.b["0"], vec![0, 1], "null branch arm count should coerce to 0");
assert_eq!(file.fn_map["0"].line, 0, "null fn line should coerce to 0");
assert_eq!(file.statement_map["0"].end.column, 0, "null position column should coerce to 0");
let empty_pos = &file.branch_map["0"].locations[1].start;
assert_eq!(empty_pos.line, 0, "missing line in empty position should default to 0");
assert_eq!(empty_pos.column, 0, "missing column in empty position should default to 0");
}
#[test]
fn parse_coverage_map_null_string_fields() {
use oxc_coverage_instrument::parse_coverage_map;
let json = r#"{
"test.js": {
"path": null,
"statementMap": {},
"fnMap": {"0": {"name": null, "line": 1, "decl": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": 10}}, "loc": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": 10}}}},
"branchMap": {"0": {"loc": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": 10}}, "line": 1, "type": null, "locations": []}},
"s": {},
"f": {"0": 0},
"b": {}
}
}"#;
let parsed = parse_coverage_map(json).unwrap();
let file = &parsed["test.js"];
assert_eq!(file.path, "", "null path should coerce to empty string");
assert_eq!(file.fn_map["0"].name, "", "null fn name should coerce to empty string");
assert_eq!(
file.branch_map["0"].branch_type, "",
"null branch type should coerce to empty string"
);
}
#[test]
fn parse_coverage_map_missing_string_fields() {
use oxc_coverage_instrument::parse_coverage_map;
let json = r#"{
"test.js": {
"statementMap": {},
"fnMap": {"0": {"line": 1, "decl": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": 10}}, "loc": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": 10}}}},
"branchMap": {"0": {"loc": {"start": {"line": 1, "column": 0}, "end": {"line": 1, "column": 10}}, "line": 1, "locations": []}},
"s": {},
"f": {"0": 0},
"b": {}
}
}"#;
let parsed = parse_coverage_map(json).unwrap();
let file = &parsed["test.js"];
assert_eq!(file.path, "", "missing path should default to empty string");
assert_eq!(file.fn_map["0"].name, "", "missing fn name should default to empty string");
assert_eq!(
file.branch_map["0"].branch_type, "",
"missing branch type should default to empty string"
);
}
#[test]
fn source_map_with_invalid_input_still_works() {
let opts = InstrumentOptions {
source_map: true,
input_source_map: Some("not valid json".to_string()),
..InstrumentOptions::default()
};
let result = instrument("function f() { return 1; }", "test.js", &opts).unwrap();
assert!(result.source_map.is_some());
}
#[test]
fn gap_object_method_gets_function_counter() {
let result = instrument_js("const obj = { foo() { return 1; }, bar() { return 2; } };");
assert!(
result.coverage_map.fn_map.len() >= 2,
"Object methods should get function counters, got {} functions: {:?}",
result.coverage_map.fn_map.len(),
result.coverage_map.fn_map.values().map(|f| &f.name).collect::<Vec<_>>()
);
}
#[test]
fn gap_getter_setter_get_function_counter() {
let result = instrument_js("const obj = { get x() { return 1; }, set x(v) { this._x = v; } };");
assert!(
result.coverage_map.fn_map.len() >= 2,
"Getters/setters should get function counters, got {} functions: {:?}",
result.coverage_map.fn_map.len(),
result.coverage_map.fn_map.values().map(|f| &f.name).collect::<Vec<_>>()
);
}
#[test]
fn class_property_initializer_gets_statement() {
let result = instrument_js("class Foo { x = 1; y = computeDefault(); }");
let stmt_count = result.coverage_map.statement_map.len();
assert!(
stmt_count >= 3,
"Class property initializers should get statement counters, got {} statements",
stmt_count
);
}
#[test]
fn private_class_property_initializer_gets_statement() {
let result = instrument_js("class Foo { #x = computeDefault(); }");
let stmt_count = result.coverage_map.statement_map.len();
assert!(
stmt_count >= 2,
"Private class property initializers should get statement counters, got {} statements",
stmt_count
);
}
#[test]
fn class_property_initializer_wraps_value() {
let result = instrument_js("class Foo {\n x = 1;\n y = computeDefault();\n}");
assert!(result.code.contains(".s["), "Should contain statement counters in class body");
}
#[test]
fn ignore_class_methods_skips_function_counter() {
let opts = InstrumentOptions {
ignore_class_methods: vec!["render".to_string(), "componentDidMount".to_string()],
..default_opts()
};
let result = instrument(
"class App { render() { return 1; } update() { return 2; } componentDidMount() { return 3; } }",
"test.js",
&opts,
).unwrap();
assert_eq!(
result.coverage_map.fn_map.len(),
1,
"Only non-ignored methods should get function counters"
);
assert_eq!(result.coverage_map.fn_map["0"].name, "update");
}
#[test]
fn ignore_class_methods_still_instruments_body() {
let opts =
InstrumentOptions { ignore_class_methods: vec!["render".to_string()], ..default_opts() };
let result =
instrument("class App { render() { const x = 1; return x; } }", "test.js", &opts).unwrap();
assert_eq!(result.coverage_map.fn_map.len(), 0);
assert!(result.coverage_map.statement_map.len() >= 3);
}
#[test]
fn ignore_class_methods_empty_list_instruments_all() {
let result = instrument_js("class App { render() { return 1; } update() { return 2; } }");
assert_eq!(result.coverage_map.fn_map.len(), 2);
}
#[test]
fn ignore_class_methods_string_literal_key() {
let opts =
InstrumentOptions { ignore_class_methods: vec!["render".to_string()], ..default_opts() };
let result = instrument(
"class App { \"render\"() { return 1; } update() { return 2; } }",
"test.js",
&opts,
)
.unwrap();
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "update");
}
#[test]
fn ignore_class_methods_with_pragma_no_leak() {
let opts =
InstrumentOptions { ignore_class_methods: vec!["render".to_string()], ..default_opts() };
let result = instrument(
"class App { /* istanbul ignore next */ render() { return 1; } update() { return 2; } }",
"test.js",
&opts,
)
.unwrap();
assert_eq!(result.coverage_map.fn_map.len(), 1, "Only update should have a function counter");
assert_eq!(result.coverage_map.fn_map["0"].name, "update");
}
#[test]
fn report_logic_adds_bt_field() {
let opts = InstrumentOptions { report_logic: true, ..default_opts() };
let result = instrument("const x = a && b;", "test.js", &opts).unwrap();
assert!(result.coverage_map.b_t.is_some(), "bT should be present when report_logic is enabled");
let b_t = result.coverage_map.b_t.unwrap();
assert_eq!(b_t.len(), 1, "Should have 1 bT entry for the logical expression");
let branch_key = b_t.keys().next().unwrap();
assert_eq!(b_t[branch_key].len(), 2, "bT entry should have 2 paths (a and b)");
}
#[test]
fn report_logic_disabled_no_bt_field() {
let result = instrument_js("const x = a && b;");
assert!(
result.coverage_map.b_t.is_none(),
"bT should not be present when report_logic is disabled"
);
}
#[test]
fn report_logic_wraps_with_helper() {
let opts = InstrumentOptions { report_logic: true, ..default_opts() };
let result = instrument("const x = a || b;", "test.js", &opts).unwrap();
assert!(result.code.contains("_bt("), "Should contain truthy tracking helper calls");
assert!(result.code.contains("_temp;"), "Should declare temp variable");
assert!(result.code.contains("function "), "Should contain helper function definition");
assert!(result.code.contains(".bT["), "Helper should reference bT counter");
assert!(
result.code.contains("!Array.isArray("),
"Should check if NOT an array (Istanbul's check)"
);
assert!(
result.code.contains("Object.values("),
"Should check Object.values length (Istanbul's check)"
);
assert!(
result.code.contains("Object.getPrototypeOf("),
"Should check prototype (Istanbul's check)"
);
}
#[test]
fn report_logic_only_for_logical_expressions() {
let opts = InstrumentOptions { report_logic: true, ..default_opts() };
let result = instrument("if (x) { a(); } else { b(); }", "test.js", &opts).unwrap();
assert!(
result.coverage_map.b_t.is_none() || result.coverage_map.b_t.as_ref().unwrap().is_empty(),
"bT should not have entries for if/else branches"
);
}
#[test]
fn report_logic_chained_logical() {
let opts = InstrumentOptions { report_logic: true, ..default_opts() };
let result = instrument("const x = a && b && c;", "test.js", &opts).unwrap();
let b_t = result.coverage_map.b_t.unwrap();
assert_eq!(b_t.len(), 1);
let entry = b_t.values().next().unwrap();
assert_eq!(entry.len(), 3, "Chained a && b && c should have 3 bT paths");
}
#[test]
fn report_logic_nullish_coalescing() {
let opts = InstrumentOptions { report_logic: true, ..default_opts() };
let result = instrument("const x = a ?? b;", "test.js", &opts).unwrap();
let b_t = result.coverage_map.b_t.unwrap();
assert_eq!(b_t.len(), 1, "Nullish coalescing should have bT entry");
}
#[test]
fn export_function_counter_is_hoisted() {
let result = instrument_js("export function foo() { return 1; }");
let export_pos = result.code.find("export").unwrap();
let counter_pos = result.code.rfind("++").unwrap_or(0);
assert!(
result.code[..export_pos].contains("++"),
"Statement counter should be hoisted before export declaration"
);
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "foo");
}
#[test]
fn export_const_arrow_counter_is_hoisted() {
let result = instrument_js("export const add = (a, b) => a + b;");
let export_pos = result.code.find("export").unwrap();
assert!(
result.code[..export_pos].contains("++"),
"Statement counter should be hoisted before export const"
);
assert_eq!(result.coverage_map.fn_map.len(), 1);
assert_eq!(result.coverage_map.fn_map["0"].name, "add");
}
#[test]
fn report_logic_json_roundtrip() {
let opts = InstrumentOptions { report_logic: true, ..default_opts() };
let result = instrument("const x = a || b;", "test.js", &opts).unwrap();
let json = serde_json::to_string(&result.coverage_map).unwrap();
assert!(json.contains("\"bT\""), "JSON should contain bT field");
let parsed: oxc_coverage_instrument::FileCoverage = serde_json::from_str(&json).unwrap();
assert!(parsed.b_t.is_some());
}