1pub mod observe;
2pub mod tsconfig;
3
4use std::sync::OnceLock;
5
6use exspec_core::extractor::{FileAnalysis, LanguageExtractor, TestAnalysis, TestFunction};
7use exspec_core::query_utils::{
8 apply_same_file_helper_tracing, collect_mock_class_names, count_captures,
9 count_captures_within_context, count_duplicate_literals,
10 extract_suppression_from_previous_line, has_any_match,
11};
12use streaming_iterator::StreamingIterator;
13use tree_sitter::{Node, Parser, Query, QueryCursor};
14
15const TEST_FUNCTION_QUERY: &str = include_str!("../queries/test_function.scm");
16const ASSERTION_QUERY: &str = include_str!("../queries/assertion.scm");
17const MOCK_USAGE_QUERY: &str = include_str!("../queries/mock_usage.scm");
18const MOCK_ASSIGNMENT_QUERY: &str = include_str!("../queries/mock_assignment.scm");
19const PARAMETERIZED_QUERY: &str = include_str!("../queries/parameterized.scm");
20const IMPORT_PBT_QUERY: &str = include_str!("../queries/import_pbt.scm");
21const IMPORT_CONTRACT_QUERY: &str = include_str!("../queries/import_contract.scm");
22const HOW_NOT_WHAT_QUERY: &str = include_str!("../queries/how_not_what.scm");
23const PRIVATE_IN_ASSERTION_QUERY: &str = include_str!("../queries/private_in_assertion.scm");
24const ERROR_TEST_QUERY: &str = include_str!("../queries/error_test.scm");
25const RELATIONAL_ASSERTION_QUERY: &str = include_str!("../queries/relational_assertion.scm");
26const WAIT_AND_SEE_QUERY: &str = include_str!("../queries/wait_and_see.scm");
27const HELPER_TRACE_QUERY: &str = include_str!("../queries/helper_trace.scm");
28
29fn ts_language() -> tree_sitter::Language {
30 tree_sitter_typescript::LANGUAGE_TSX.into()
31}
32
33fn cached_query<'a>(lock: &'a OnceLock<Query>, source: &str) -> &'a Query {
34 lock.get_or_init(|| Query::new(&ts_language(), source).expect("invalid query"))
35}
36
37static TEST_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
38static ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
39static MOCK_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
40static MOCK_ASSIGN_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
41static PARAMETERIZED_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
42static IMPORT_PBT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
43static IMPORT_CONTRACT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
44static HOW_NOT_WHAT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
45static PRIVATE_IN_ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
46static ERROR_TEST_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
47static RELATIONAL_ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
48static WAIT_AND_SEE_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
49static HELPER_TRACE_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
50
51pub struct TypeScriptExtractor;
52
53impl TypeScriptExtractor {
54 pub fn new() -> Self {
55 Self
56 }
57
58 pub fn parser() -> Parser {
59 let mut parser = Parser::new();
60 let language = tree_sitter_typescript::LANGUAGE_TSX;
61 parser
62 .set_language(&language.into())
63 .expect("failed to load TypeScript grammar");
64 parser
65 }
66}
67
68impl Default for TypeScriptExtractor {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74fn count_enclosing_describe_fixtures(root: Node, test_start_byte: usize, source: &[u8]) -> usize {
79 let Some(start_node) = root.descendant_for_byte_range(test_start_byte, test_start_byte) else {
80 return 0;
81 };
82
83 let mut count = 0;
84 let mut current = start_node.parent();
85 while let Some(node) = current {
86 if node.kind() == "statement_block" && is_describe_callback_body(node, source) {
88 let child_count = node.named_child_count();
90 for i in 0..child_count {
91 if let Some(child) = node.named_child(i) {
92 let kind = child.kind();
93 if kind == "lexical_declaration" || kind == "variable_declaration" {
94 let declarator_count = (0..child.named_child_count())
97 .filter_map(|j| child.named_child(j))
98 .filter(|c| c.kind() == "variable_declarator")
99 .count();
100 count += declarator_count;
101 }
102 }
103 }
104 }
105 current = node.parent();
106 }
107
108 count
109}
110
111fn is_describe_callback_body(block: Node, source: &[u8]) -> bool {
114 let parent = match block.parent() {
115 Some(p) => p,
116 None => return false,
117 };
118 let kind = parent.kind();
119 if kind != "arrow_function" && kind != "function_expression" {
120 return false;
121 }
122 let args = match parent.parent() {
123 Some(p) if p.kind() == "arguments" => p,
124 _ => return false,
125 };
126 let call = match args.parent() {
127 Some(p) if p.kind() == "call_expression" => p,
128 _ => return false,
129 };
130 if let Some(func_node) = call.child_by_field_name("function") {
132 if let Ok(name) = func_node.utf8_text(source) {
133 return name == "describe" || name.starts_with("describe.");
134 }
135 }
136 false
137}
138
139fn extract_mock_class_name(var_name: &str) -> String {
140 if let Some(stripped) = var_name.strip_prefix("mock") {
142 if !stripped.is_empty() && stripped.starts_with(|c: char| c.is_uppercase()) {
143 return stripped.to_string();
144 }
145 }
146 var_name.to_string()
147}
148
149struct TestMatch {
150 name: String,
151 fn_start_byte: usize,
152 fn_end_byte: usize,
153 fn_start_row: usize,
154 fn_end_row: usize,
155}
156
157fn extract_functions_from_tree(source: &str, file_path: &str, root: Node) -> Vec<TestFunction> {
158 let test_query = cached_query(&TEST_QUERY_CACHE, TEST_FUNCTION_QUERY);
159 let assertion_query = cached_query(&ASSERTION_QUERY_CACHE, ASSERTION_QUERY);
160 let mock_query = cached_query(&MOCK_QUERY_CACHE, MOCK_USAGE_QUERY);
161 let mock_assign_query = cached_query(&MOCK_ASSIGN_QUERY_CACHE, MOCK_ASSIGNMENT_QUERY);
162 let how_not_what_query = cached_query(&HOW_NOT_WHAT_QUERY_CACHE, HOW_NOT_WHAT_QUERY);
163 let private_query = cached_query(
164 &PRIVATE_IN_ASSERTION_QUERY_CACHE,
165 PRIVATE_IN_ASSERTION_QUERY,
166 );
167 let wait_query = cached_query(&WAIT_AND_SEE_QUERY_CACHE, WAIT_AND_SEE_QUERY);
168
169 let name_idx = test_query
170 .capture_index_for_name("name")
171 .expect("no @name capture");
172 let function_idx = test_query
173 .capture_index_for_name("function")
174 .expect("no @function capture");
175
176 let source_bytes = source.as_bytes();
177
178 let mut test_matches = Vec::new();
179 {
180 let mut cursor = QueryCursor::new();
181 let mut matches = cursor.matches(test_query, root, source_bytes);
182 while let Some(m) = matches.next() {
183 let name_capture = match m.captures.iter().find(|c| c.index == name_idx) {
184 Some(c) => c,
185 None => continue,
186 };
187 let name = match name_capture.node.utf8_text(source_bytes) {
188 Ok(s) => s.to_string(),
189 Err(_) => continue,
190 };
191
192 let fn_capture = match m.captures.iter().find(|c| c.index == function_idx) {
193 Some(c) => c,
194 None => continue,
195 };
196
197 test_matches.push(TestMatch {
198 name,
199 fn_start_byte: fn_capture.node.start_byte(),
200 fn_end_byte: fn_capture.node.end_byte(),
201 fn_start_row: fn_capture.node.start_position().row,
202 fn_end_row: fn_capture.node.end_position().row,
203 });
204 }
205 }
206
207 let mut functions = Vec::new();
208 for tm in &test_matches {
209 let fn_node = match root.descendant_for_byte_range(tm.fn_start_byte, tm.fn_end_byte) {
210 Some(n) => n,
211 None => continue,
212 };
213
214 let line = tm.fn_start_row + 1;
215 let end_line = tm.fn_end_row + 1;
216 let line_count = end_line - line + 1;
217
218 let assertion_count = count_captures(assertion_query, "assertion", fn_node, source_bytes);
219 let mock_count = count_captures(mock_query, "mock", fn_node, source_bytes);
220 let mock_classes = collect_mock_class_names(
221 mock_assign_query,
222 fn_node,
223 source_bytes,
224 extract_mock_class_name,
225 );
226
227 let how_not_what_count =
228 count_captures(how_not_what_query, "how_pattern", fn_node, source_bytes);
229
230 let private_in_assertion_count = count_captures_within_context(
231 assertion_query,
232 "assertion",
233 private_query,
234 "private_access",
235 fn_node,
236 source_bytes,
237 );
238
239 let fixture_count = count_enclosing_describe_fixtures(root, tm.fn_start_byte, source_bytes);
240
241 let has_wait = has_any_match(wait_query, "wait", fn_node, source_bytes);
243
244 let duplicate_literal_count = count_duplicate_literals(
246 assertion_query,
247 fn_node,
248 source_bytes,
249 &["number", "string"],
250 );
251
252 let suppressed_rules = extract_suppression_from_previous_line(source, tm.fn_start_row);
253
254 functions.push(TestFunction {
255 name: tm.name.clone(),
256 file: file_path.to_string(),
257 line,
258 end_line,
259 analysis: TestAnalysis {
260 assertion_count,
261 mock_count,
262 mock_classes,
263 line_count,
264 how_not_what_count: how_not_what_count + private_in_assertion_count,
265 fixture_count,
266 has_wait,
267 has_skip_call: false,
268 assertion_message_count: assertion_count, duplicate_literal_count,
270 suppressed_rules,
271 },
272 });
273 }
274
275 functions
276}
277
278impl LanguageExtractor for TypeScriptExtractor {
279 fn extract_test_functions(&self, source: &str, file_path: &str) -> Vec<TestFunction> {
280 let mut parser = Self::parser();
281 let tree = match parser.parse(source, None) {
282 Some(t) => t,
283 None => return Vec::new(),
284 };
285 extract_functions_from_tree(source, file_path, tree.root_node())
286 }
287
288 fn extract_file_analysis(&self, source: &str, file_path: &str) -> FileAnalysis {
289 let mut parser = Self::parser();
290 let tree = match parser.parse(source, None) {
291 Some(t) => t,
292 None => {
293 return FileAnalysis {
294 file: file_path.to_string(),
295 functions: Vec::new(),
296 has_pbt_import: false,
297 has_contract_import: false,
298 has_error_test: false,
299 has_relational_assertion: false,
300 parameterized_count: 0,
301 };
302 }
303 };
304
305 let root = tree.root_node();
306 let source_bytes = source.as_bytes();
307
308 let functions = extract_functions_from_tree(source, file_path, root);
309
310 let param_query = cached_query(&PARAMETERIZED_QUERY_CACHE, PARAMETERIZED_QUERY);
311 let parameterized_count = count_captures(param_query, "parameterized", root, source_bytes);
312
313 let pbt_query = cached_query(&IMPORT_PBT_QUERY_CACHE, IMPORT_PBT_QUERY);
314 let has_pbt_import = has_any_match(pbt_query, "pbt_import", root, source_bytes);
315
316 let contract_query = cached_query(&IMPORT_CONTRACT_QUERY_CACHE, IMPORT_CONTRACT_QUERY);
317 let has_contract_import =
318 has_any_match(contract_query, "contract_import", root, source_bytes);
319
320 let error_test_query = cached_query(&ERROR_TEST_QUERY_CACHE, ERROR_TEST_QUERY);
321 let has_error_test = has_any_match(error_test_query, "error_test", root, source_bytes);
322
323 let relational_query = cached_query(
324 &RELATIONAL_ASSERTION_QUERY_CACHE,
325 RELATIONAL_ASSERTION_QUERY,
326 );
327 let has_relational_assertion =
328 has_any_match(relational_query, "relational", root, source_bytes);
329
330 let mut file_analysis = FileAnalysis {
331 file: file_path.to_string(),
332 functions,
333 has_pbt_import,
334 has_contract_import,
335 has_error_test,
336 has_relational_assertion,
337 parameterized_count,
338 };
339
340 let helper_trace_query = cached_query(&HELPER_TRACE_QUERY_CACHE, HELPER_TRACE_QUERY);
344 let assertion_query_for_trace = cached_query(&ASSERTION_QUERY_CACHE, ASSERTION_QUERY);
345 apply_same_file_helper_tracing(
346 &mut file_analysis,
347 &tree,
348 source_bytes,
349 helper_trace_query,
350 helper_trace_query,
351 assertion_query_for_trace,
352 );
353
354 file_analysis
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 fn fixture(name: &str) -> String {
363 let path = format!(
364 "{}/tests/fixtures/typescript/{}",
365 env!("CARGO_MANIFEST_DIR").replace("/crates/lang-typescript", ""),
366 name
367 );
368 std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"))
369 }
370
371 #[test]
374 fn parse_typescript_source() {
375 let source = "const x: number = 42;\n";
376 let mut parser = TypeScriptExtractor::parser();
377 let tree = parser.parse(source, None).unwrap();
378 assert_eq!(tree.root_node().kind(), "program");
379 }
380
381 #[test]
382 fn typescript_extractor_implements_language_extractor() {
383 let extractor = TypeScriptExtractor::new();
384 let _: &dyn exspec_core::extractor::LanguageExtractor = &extractor;
385 }
386
387 #[test]
390 fn extract_single_test_function() {
391 let source = fixture("t001_pass.test.ts");
392 let extractor = TypeScriptExtractor::new();
393 let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
394 assert_eq!(funcs.len(), 1);
395 assert_eq!(funcs[0].name, "create user");
396 assert_eq!(funcs[0].line, 1);
397 }
398
399 #[test]
400 fn extract_multiple_tests_excludes_helpers_and_describe() {
401 let source = fixture("multiple_tests.test.ts");
402 let extractor = TypeScriptExtractor::new();
403 let funcs = extractor.extract_test_functions(&source, "multiple_tests.test.ts");
404 assert_eq!(funcs.len(), 3);
405 let names: Vec<&str> = funcs.iter().map(|f| f.name.as_str()).collect();
406 assert_eq!(
407 names,
408 vec!["adds numbers", "subtracts numbers", "multiplies numbers"]
409 );
410 }
411
412 #[test]
413 fn line_count_calculation() {
414 let source = fixture("t001_pass.test.ts");
415 let extractor = TypeScriptExtractor::new();
416 let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
417 assert_eq!(
418 funcs[0].analysis.line_count,
419 funcs[0].end_line - funcs[0].line + 1
420 );
421 }
422
423 #[test]
424 fn violation_file_extracts_function() {
425 let source = fixture("t001_violation.test.ts");
426 let extractor = TypeScriptExtractor::new();
427 let funcs = extractor.extract_test_functions(&source, "t001_violation.test.ts");
428 assert_eq!(funcs.len(), 1);
429 assert_eq!(funcs[0].name, "create user");
430 }
431
432 #[test]
435 fn assertion_count_zero_for_violation() {
436 let source = fixture("t001_violation.test.ts");
437 let extractor = TypeScriptExtractor::new();
438 let funcs = extractor.extract_test_functions(&source, "t001_violation.test.ts");
439 assert_eq!(funcs[0].analysis.assertion_count, 0);
440 }
441
442 #[test]
443 fn assertion_count_positive_for_pass() {
444 let source = fixture("t001_pass.test.ts");
445 let extractor = TypeScriptExtractor::new();
446 let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
447 assert!(funcs[0].analysis.assertion_count >= 1);
448 }
449
450 #[test]
453 fn tsx_file_detects_assertions() {
454 let source = fixture("t001_tsx_assertion.test.tsx");
455 let extractor = TypeScriptExtractor::new();
456 let funcs = extractor.extract_test_functions(&source, "t001_tsx_assertion.test.tsx");
457 assert_eq!(
458 funcs.len(),
459 2,
460 "should extract 2 test functions from TSX file"
461 );
462 for f in &funcs {
463 assert!(
464 f.analysis.assertion_count >= 1,
465 "test '{}' should have assertions detected in TSX file, got {}",
466 f.name,
467 f.analysis.assertion_count
468 );
469 }
470 }
471
472 #[test]
475 fn mock_count_for_violation() {
476 let source = fixture("t002_violation.test.ts");
477 let extractor = TypeScriptExtractor::new();
478 let funcs = extractor.extract_test_functions(&source, "t002_violation.test.ts");
479 assert_eq!(funcs.len(), 1);
480 assert_eq!(funcs[0].analysis.mock_count, 6);
481 }
482
483 #[test]
484 fn mock_count_for_pass() {
485 let source = fixture("t002_pass.test.ts");
486 let extractor = TypeScriptExtractor::new();
487 let funcs = extractor.extract_test_functions(&source, "t002_pass.test.ts");
488 assert_eq!(funcs.len(), 1);
489 assert_eq!(funcs[0].analysis.mock_count, 1);
490 assert_eq!(funcs[0].analysis.mock_classes, vec!["Db"]);
491 }
492
493 #[test]
494 fn mock_class_name_extraction() {
495 assert_eq!(extract_mock_class_name("mockDb"), "Db");
496 assert_eq!(
497 extract_mock_class_name("mockPaymentService"),
498 "PaymentService"
499 );
500 assert_eq!(extract_mock_class_name("myMock"), "myMock");
501 }
502
503 #[test]
506 fn suppressed_test_has_suppressed_rules() {
507 let source = fixture("suppressed.test.ts");
508 let extractor = TypeScriptExtractor::new();
509 let funcs = extractor.extract_test_functions(&source, "suppressed.test.ts");
510 assert_eq!(funcs.len(), 1);
511 assert_eq!(funcs[0].analysis.mock_count, 6);
512 assert!(funcs[0]
513 .analysis
514 .suppressed_rules
515 .iter()
516 .any(|r| r.0 == "T002"));
517 }
518
519 #[test]
520 fn non_suppressed_test_has_empty_suppressed_rules() {
521 let source = fixture("t002_violation.test.ts");
522 let extractor = TypeScriptExtractor::new();
523 let funcs = extractor.extract_test_functions(&source, "t002_violation.test.ts");
524 assert!(funcs[0].analysis.suppressed_rules.is_empty());
525 }
526
527 #[test]
530 fn giant_test_line_count() {
531 let source = fixture("t003_violation.test.ts");
532 let extractor = TypeScriptExtractor::new();
533 let funcs = extractor.extract_test_functions(&source, "t003_violation.test.ts");
534 assert_eq!(funcs.len(), 1);
535 assert!(funcs[0].analysis.line_count > 50);
536 }
537
538 #[test]
539 fn short_test_line_count() {
540 let source = fixture("t003_pass.test.ts");
541 let extractor = TypeScriptExtractor::new();
542 let funcs = extractor.extract_test_functions(&source, "t003_pass.test.ts");
543 assert_eq!(funcs.len(), 1);
544 assert!(funcs[0].analysis.line_count <= 50);
545 }
546
547 #[test]
550 fn file_analysis_detects_parameterized() {
551 let source = fixture("t004_pass.test.ts");
552 let extractor = TypeScriptExtractor::new();
553 let fa = extractor.extract_file_analysis(&source, "t004_pass.test.ts");
554 assert!(
555 fa.parameterized_count >= 1,
556 "expected parameterized_count >= 1, got {}",
557 fa.parameterized_count
558 );
559 }
560
561 #[test]
562 fn file_analysis_no_parameterized() {
563 let source = fixture("t004_violation.test.ts");
564 let extractor = TypeScriptExtractor::new();
565 let fa = extractor.extract_file_analysis(&source, "t004_violation.test.ts");
566 assert_eq!(fa.parameterized_count, 0);
567 }
568
569 #[test]
572 fn file_analysis_detects_pbt_import() {
573 let source = fixture("t005_pass.test.ts");
574 let extractor = TypeScriptExtractor::new();
575 let fa = extractor.extract_file_analysis(&source, "t005_pass.test.ts");
576 assert!(fa.has_pbt_import);
577 }
578
579 #[test]
580 fn file_analysis_no_pbt_import() {
581 let source = fixture("t005_violation.test.ts");
582 let extractor = TypeScriptExtractor::new();
583 let fa = extractor.extract_file_analysis(&source, "t005_violation.test.ts");
584 assert!(!fa.has_pbt_import);
585 }
586
587 #[test]
590 fn file_analysis_detects_contract_import() {
591 let source = fixture("t008_pass.test.ts");
592 let extractor = TypeScriptExtractor::new();
593 let fa = extractor.extract_file_analysis(&source, "t008_pass.test.ts");
594 assert!(fa.has_contract_import);
595 }
596
597 #[test]
598 fn file_analysis_no_contract_import() {
599 let source = fixture("t008_violation.test.ts");
600 let extractor = TypeScriptExtractor::new();
601 let fa = extractor.extract_file_analysis(&source, "t008_violation.test.ts");
602 assert!(!fa.has_contract_import);
603 }
604
605 #[test]
608 fn suppression_on_describe_does_not_apply_to_inner_tests() {
609 let source = fixture("describe_suppression.test.ts");
610 let extractor = TypeScriptExtractor::new();
611 let funcs = extractor.extract_test_functions(&source, "describe_suppression.test.ts");
612 assert_eq!(funcs.len(), 2, "expected 2 test functions inside describe");
613 for f in &funcs {
614 assert!(
615 f.analysis.suppressed_rules.is_empty(),
616 "test '{}' should NOT have suppressed rules (suppression on describe does not propagate)",
617 f.name
618 );
619 assert_eq!(
620 f.analysis.assertion_count, 0,
621 "test '{}' should have 0 assertions (T001 violation expected)",
622 f.name
623 );
624 }
625 }
626
627 #[test]
630 fn file_analysis_preserves_test_functions() {
631 let source = fixture("t001_pass.test.ts");
632 let extractor = TypeScriptExtractor::new();
633 let fa = extractor.extract_file_analysis(&source, "t001_pass.test.ts");
634 assert_eq!(fa.functions.len(), 1);
635 assert_eq!(fa.functions[0].name, "create user");
636 }
637
638 #[test]
641 fn how_not_what_count_for_violation() {
642 let source = fixture("t101_violation.test.ts");
643 let extractor = TypeScriptExtractor::new();
644 let funcs = extractor.extract_test_functions(&source, "t101_violation.test.ts");
645 assert_eq!(funcs.len(), 2);
646 assert!(
647 funcs[0].analysis.how_not_what_count > 0,
648 "expected how_not_what_count > 0 for first test, got {}",
649 funcs[0].analysis.how_not_what_count
650 );
651 assert!(
652 funcs[1].analysis.how_not_what_count > 0,
653 "expected how_not_what_count > 0 for second test, got {}",
654 funcs[1].analysis.how_not_what_count
655 );
656 }
657
658 #[test]
659 fn how_not_what_count_zero_for_pass() {
660 let source = fixture("t101_pass.test.ts");
661 let extractor = TypeScriptExtractor::new();
662 let funcs = extractor.extract_test_functions(&source, "t101_pass.test.ts");
663 assert_eq!(funcs.len(), 1);
664 assert_eq!(funcs[0].analysis.how_not_what_count, 0);
665 }
666
667 #[test]
668 fn how_not_what_coexists_with_assertions() {
669 let source = fixture("t101_violation.test.ts");
670 let extractor = TypeScriptExtractor::new();
671 let funcs = extractor.extract_test_functions(&source, "t101_violation.test.ts");
672 assert!(
673 funcs[0].analysis.assertion_count > 0,
674 "should also count as assertions"
675 );
676 assert!(
677 funcs[0].analysis.how_not_what_count > 0,
678 "should count as how-not-what"
679 );
680 }
681
682 fn make_query(scm: &str) -> Query {
685 Query::new(&ts_language(), scm).unwrap()
686 }
687
688 #[test]
689 fn query_capture_names_test_function() {
690 let q = make_query(include_str!("../queries/test_function.scm"));
691 assert!(
692 q.capture_index_for_name("name").is_some(),
693 "test_function.scm must define @name capture"
694 );
695 assert!(
696 q.capture_index_for_name("function").is_some(),
697 "test_function.scm must define @function capture"
698 );
699 }
700
701 #[test]
702 fn query_capture_names_assertion() {
703 let q = make_query(include_str!("../queries/assertion.scm"));
704 assert!(
705 q.capture_index_for_name("assertion").is_some(),
706 "assertion.scm must define @assertion capture"
707 );
708 }
709
710 #[test]
711 fn query_capture_names_mock_usage() {
712 let q = make_query(include_str!("../queries/mock_usage.scm"));
713 assert!(
714 q.capture_index_for_name("mock").is_some(),
715 "mock_usage.scm must define @mock capture"
716 );
717 }
718
719 #[test]
720 fn query_capture_names_mock_assignment() {
721 let q = make_query(include_str!("../queries/mock_assignment.scm"));
722 assert!(
723 q.capture_index_for_name("var_name").is_some(),
724 "mock_assignment.scm must define @var_name (required by collect_mock_class_names .expect())"
725 );
726 }
727
728 #[test]
729 fn query_capture_names_parameterized() {
730 let q = make_query(include_str!("../queries/parameterized.scm"));
731 assert!(
732 q.capture_index_for_name("parameterized").is_some(),
733 "parameterized.scm must define @parameterized capture"
734 );
735 }
736
737 #[test]
738 fn query_capture_names_import_pbt() {
739 let q = make_query(include_str!("../queries/import_pbt.scm"));
740 assert!(
741 q.capture_index_for_name("pbt_import").is_some(),
742 "import_pbt.scm must define @pbt_import capture"
743 );
744 }
745
746 #[test]
747 fn query_capture_names_import_contract() {
748 let q = make_query(include_str!("../queries/import_contract.scm"));
749 assert!(
750 q.capture_index_for_name("contract_import").is_some(),
751 "import_contract.scm must define @contract_import capture"
752 );
753 }
754
755 #[test]
756 fn query_capture_names_how_not_what() {
757 let q = make_query(include_str!("../queries/how_not_what.scm"));
758 assert!(
759 q.capture_index_for_name("how_pattern").is_some(),
760 "how_not_what.scm must define @how_pattern capture"
761 );
762 }
763
764 #[test]
767 fn fixture_count_for_violation() {
768 let source = fixture("t102_violation.test.ts");
769 let extractor = TypeScriptExtractor::new();
770 let funcs = extractor.extract_test_functions(&source, "t102_violation.test.ts");
771 assert_eq!(funcs.len(), 1);
772 assert_eq!(
773 funcs[0].analysis.fixture_count, 6,
774 "expected 6 describe-level let declarations"
775 );
776 }
777
778 #[test]
779 fn fixture_count_for_pass() {
780 let source = fixture("t102_pass.test.ts");
781 let extractor = TypeScriptExtractor::new();
782 let funcs = extractor.extract_test_functions(&source, "t102_pass.test.ts");
783 assert_eq!(funcs.len(), 1);
784 assert_eq!(
785 funcs[0].analysis.fixture_count, 2,
786 "expected 2 describe-level let declarations"
787 );
788 }
789
790 #[test]
791 fn fixture_count_nested_describe() {
792 let source = fixture("t102_nested.test.ts");
793 let extractor = TypeScriptExtractor::new();
794 let funcs = extractor.extract_test_functions(&source, "t102_nested.test.ts");
795 assert_eq!(funcs.len(), 2);
796 let inner = funcs
798 .iter()
799 .find(|f| f.name == "test in nested describe inherits all fixtures")
800 .unwrap();
801 assert_eq!(
802 inner.analysis.fixture_count, 6,
803 "inner test should see outer + inner fixtures"
804 );
805 let outer = funcs
807 .iter()
808 .find(|f| f.name == "test in outer describe only sees outer fixtures")
809 .unwrap();
810 assert_eq!(
811 outer.analysis.fixture_count, 3,
812 "outer test should see only outer fixtures"
813 );
814 }
815
816 #[test]
817 fn fixture_count_describe_each() {
818 let source = fixture("t102_describe_each.test.ts");
819 let extractor = TypeScriptExtractor::new();
820 let funcs = extractor.extract_test_functions(&source, "t102_describe_each.test.ts");
821 assert_eq!(funcs.len(), 1);
822 assert_eq!(
823 funcs[0].analysis.fixture_count, 2,
824 "describe.each should be recognized as describe scope"
825 );
826 }
827
828 #[test]
829 fn fixture_count_top_level_test_zero() {
830 let source = "it('standalone test', () => { expect(1).toBe(1); });";
832 let extractor = TypeScriptExtractor::new();
833 let funcs = extractor.extract_test_functions(source, "top_level.test.ts");
834 assert_eq!(funcs.len(), 1);
835 assert_eq!(
836 funcs[0].analysis.fixture_count, 0,
837 "top-level test should have 0 fixtures"
838 );
839 }
840
841 #[test]
844 fn private_dot_notation_detected() {
845 let source = fixture("t101_private_violation.test.ts");
846 let extractor = TypeScriptExtractor::new();
847 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
848 let func = funcs
850 .iter()
851 .find(|f| f.name == "checks internal count via dot notation")
852 .unwrap();
853 assert!(
854 func.analysis.how_not_what_count >= 2,
855 "expected >= 2 private access in assertions (dot), got {}",
856 func.analysis.how_not_what_count
857 );
858 }
859
860 #[test]
861 fn private_bracket_notation_detected() {
862 let source = fixture("t101_private_violation.test.ts");
863 let extractor = TypeScriptExtractor::new();
864 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
865 let func = funcs
867 .iter()
868 .find(|f| f.name == "checks internal via bracket notation")
869 .unwrap();
870 assert!(
871 func.analysis.how_not_what_count >= 2,
872 "expected >= 2 private access in assertions (bracket), got {}",
873 func.analysis.how_not_what_count
874 );
875 }
876
877 #[test]
878 fn private_outside_expect_not_counted() {
879 let source = fixture("t101_private_violation.test.ts");
880 let extractor = TypeScriptExtractor::new();
881 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
882 let func = funcs
884 .iter()
885 .find(|f| f.name == "private outside expect not counted")
886 .unwrap();
887 assert_eq!(
888 func.analysis.how_not_what_count, 0,
889 "private access outside expect should not count"
890 );
891 }
892
893 #[test]
894 fn private_adds_to_how_not_what() {
895 let source = fixture("t101_private_violation.test.ts");
896 let extractor = TypeScriptExtractor::new();
897 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
898 let func = funcs
900 .iter()
901 .find(|f| f.name == "mixed private and mock verification")
902 .unwrap();
903 assert!(
904 func.analysis.how_not_what_count >= 2,
905 "expected mock (1) + private (1) = >= 2, got {}",
906 func.analysis.how_not_what_count
907 );
908 }
909
910 #[test]
911 fn query_capture_names_private_in_assertion() {
912 let q = make_query(include_str!("../queries/private_in_assertion.scm"));
913 assert!(
914 q.capture_index_for_name("private_access").is_some(),
915 "private_in_assertion.scm must define @private_access capture"
916 );
917 }
918
919 #[test]
922 fn error_test_to_throw() {
923 let source = fixture("t103_pass_toThrow.test.ts");
924 let extractor = TypeScriptExtractor::new();
925 let fa = extractor.extract_file_analysis(&source, "t103_pass_toThrow.test.ts");
926 assert!(fa.has_error_test, ".toThrow() should set has_error_test");
927 }
928
929 #[test]
930 fn error_test_to_throw_error() {
931 let source = fixture("t103_pass_toThrowError.test.ts");
932 let extractor = TypeScriptExtractor::new();
933 let fa = extractor.extract_file_analysis(&source, "t103_pass_toThrowError.test.ts");
934 assert!(
935 fa.has_error_test,
936 ".toThrowError() should set has_error_test"
937 );
938 }
939
940 #[test]
941 fn error_test_rejects() {
942 let source = fixture("t103_pass_rejects.test.ts");
943 let extractor = TypeScriptExtractor::new();
944 let fa = extractor.extract_file_analysis(&source, "t103_pass_rejects.test.ts");
945 assert!(fa.has_error_test, ".rejects should set has_error_test");
946 }
947
948 #[test]
949 fn error_test_false_positive_rejects_property() {
950 let source = fixture("t103_false_positive_rejects_property.test.ts");
951 let extractor = TypeScriptExtractor::new();
952 let fa = extractor
953 .extract_file_analysis(&source, "t103_false_positive_rejects_property.test.ts");
954 assert!(
955 !fa.has_error_test,
956 "service.rejects should NOT set has_error_test"
957 );
958 }
959
960 #[test]
961 fn error_test_no_patterns() {
962 let source = fixture("t103_violation.test.ts");
963 let extractor = TypeScriptExtractor::new();
964 let fa = extractor.extract_file_analysis(&source, "t103_violation.test.ts");
965 assert!(
966 !fa.has_error_test,
967 "no error patterns should set has_error_test=false"
968 );
969 }
970
971 #[test]
972 fn query_capture_names_error_test() {
973 let q = make_query(include_str!("../queries/error_test.scm"));
974 assert!(
975 q.capture_index_for_name("error_test").is_some(),
976 "error_test.scm must define @error_test capture"
977 );
978 }
979
980 #[test]
983 fn relational_assertion_violation() {
984 let source = fixture("t105_violation.test.ts");
985 let extractor = TypeScriptExtractor::new();
986 let fa = extractor.extract_file_analysis(&source, "t105_violation.test.ts");
987 assert!(
988 !fa.has_relational_assertion,
989 "all toBe/toEqual file should not have relational"
990 );
991 }
992
993 #[test]
994 fn relational_assertion_pass_greater_than() {
995 let source = fixture("t105_pass_relational.test.ts");
996 let extractor = TypeScriptExtractor::new();
997 let fa = extractor.extract_file_analysis(&source, "t105_pass_relational.test.ts");
998 assert!(
999 fa.has_relational_assertion,
1000 "toBeGreaterThan should set has_relational_assertion"
1001 );
1002 }
1003
1004 #[test]
1005 fn relational_assertion_pass_truthy() {
1006 let source = fixture("t105_pass_truthy.test.ts");
1007 let extractor = TypeScriptExtractor::new();
1008 let fa = extractor.extract_file_analysis(&source, "t105_pass_truthy.test.ts");
1009 assert!(
1010 fa.has_relational_assertion,
1011 "toBeTruthy should set has_relational_assertion"
1012 );
1013 }
1014
1015 #[test]
1016 fn query_capture_names_relational_assertion() {
1017 let q = make_query(include_str!("../queries/relational_assertion.scm"));
1018 assert!(
1019 q.capture_index_for_name("relational").is_some(),
1020 "relational_assertion.scm must define @relational capture"
1021 );
1022 }
1023
1024 #[test]
1027 fn wait_and_see_violation_sleep() {
1028 let source = fixture("t108_violation_sleep.test.ts");
1029 let extractor = TypeScriptExtractor::new();
1030 let funcs = extractor.extract_test_functions(&source, "t108_violation_sleep.test.ts");
1031 assert!(!funcs.is_empty());
1032 for func in &funcs {
1033 assert!(
1034 func.analysis.has_wait,
1035 "test '{}' should have has_wait=true",
1036 func.name
1037 );
1038 }
1039 }
1040
1041 #[test]
1042 fn wait_and_see_pass_no_sleep() {
1043 let source = fixture("t108_pass_no_sleep.test.ts");
1044 let extractor = TypeScriptExtractor::new();
1045 let funcs = extractor.extract_test_functions(&source, "t108_pass_no_sleep.test.ts");
1046 assert_eq!(funcs.len(), 1);
1047 assert!(
1048 !funcs[0].analysis.has_wait,
1049 "test without sleep should have has_wait=false"
1050 );
1051 }
1052
1053 #[test]
1054 fn query_capture_names_wait_and_see() {
1055 let q = make_query(include_str!("../queries/wait_and_see.scm"));
1056 assert!(
1057 q.capture_index_for_name("wait").is_some(),
1058 "wait_and_see.scm must define @wait capture"
1059 );
1060 }
1061
1062 #[test]
1065 fn t109_violation_names_detected() {
1066 let source = fixture("t109_violation.test.ts");
1067 let extractor = TypeScriptExtractor::new();
1068 let funcs = extractor.extract_test_functions(&source, "t109_violation.test.ts");
1069 assert!(!funcs.is_empty());
1070 for func in &funcs {
1071 assert!(
1072 exspec_core::rules::is_undescriptive_test_name(&func.name),
1073 "test '{}' should be undescriptive",
1074 func.name
1075 );
1076 }
1077 }
1078
1079 #[test]
1080 fn t109_pass_descriptive_names() {
1081 let source = fixture("t109_pass.test.ts");
1082 let extractor = TypeScriptExtractor::new();
1083 let funcs = extractor.extract_test_functions(&source, "t109_pass.test.ts");
1084 assert!(!funcs.is_empty());
1085 for func in &funcs {
1086 assert!(
1087 !exspec_core::rules::is_undescriptive_test_name(&func.name),
1088 "test '{}' should be descriptive",
1089 func.name
1090 );
1091 }
1092 }
1093
1094 #[test]
1096 fn t109_cjk_pass_descriptive_names() {
1097 let source = fixture("t109_cjk_pass.test.ts");
1098 let extractor = TypeScriptExtractor::new();
1099 let funcs = extractor.extract_test_functions(&source, "t109_cjk_pass.test.ts");
1100 assert!(!funcs.is_empty());
1101 for func in &funcs {
1102 assert!(
1103 !exspec_core::rules::is_undescriptive_test_name(&func.name),
1104 "CJK test '{}' should be descriptive",
1105 func.name
1106 );
1107 }
1108 }
1109
1110 #[test]
1113 fn t106_violation_duplicate_literal() {
1114 let source = fixture("t106_violation.test.ts");
1115 let extractor = TypeScriptExtractor::new();
1116 let funcs = extractor.extract_test_functions(&source, "t106_violation.test.ts");
1117 assert_eq!(funcs.len(), 1);
1118 assert!(
1119 funcs[0].analysis.duplicate_literal_count >= 3,
1120 "42 appears 3 times, should be >= 3: got {}",
1121 funcs[0].analysis.duplicate_literal_count
1122 );
1123 }
1124
1125 #[test]
1126 fn t106_pass_no_duplicates() {
1127 let source = fixture("t106_pass_no_duplicates.test.ts");
1128 let extractor = TypeScriptExtractor::new();
1129 let funcs = extractor.extract_test_functions(&source, "t106_pass_no_duplicates.test.ts");
1130 assert_eq!(funcs.len(), 1);
1131 assert!(
1132 funcs[0].analysis.duplicate_literal_count < 3,
1133 "each literal appears once: got {}",
1134 funcs[0].analysis.duplicate_literal_count
1135 );
1136 }
1137
1138 #[test]
1141 fn t001_expect_to_throw_already_covered() {
1142 let source = "import { it, expect } from 'vitest';\nit('throws', () => { expect(() => fn()).toThrow(); });";
1144 let extractor = TypeScriptExtractor::new();
1145 let funcs = extractor.extract_test_functions(&source, "test_throw.test.ts");
1146 assert_eq!(funcs.len(), 1);
1147 assert!(
1148 funcs[0].analysis.assertion_count >= 1,
1149 "expect().toThrow() should already be covered, got {}",
1150 funcs[0].analysis.assertion_count
1151 );
1152 }
1153
1154 #[test]
1155 fn t001_rejects_to_throw_counts_as_assertion() {
1156 let source = fixture("t001_rejects_to_throw.test.ts");
1158 let extractor = TypeScriptExtractor::new();
1159 let funcs = extractor.extract_test_functions(&source, "t001_rejects_to_throw.test.ts");
1160 assert_eq!(funcs.len(), 1);
1161 assert!(
1162 funcs[0].analysis.assertion_count >= 1,
1163 "expect().rejects.toThrow() should count as assertion, got {}",
1164 funcs[0].analysis.assertion_count
1165 );
1166 }
1167
1168 #[test]
1169 fn t001_expect_type_of_counts_as_assertion() {
1170 let source = fixture("t001_expect_type_of.test.ts");
1172 let extractor = TypeScriptExtractor::new();
1173 let funcs = extractor.extract_test_functions(&source, "t001_expect_type_of.test.ts");
1174 assert_eq!(funcs.len(), 1);
1175 assert!(
1176 funcs[0].analysis.assertion_count >= 1,
1177 "expectTypeOf() should count as assertion, got {}",
1178 funcs[0].analysis.assertion_count
1179 );
1180 }
1181
1182 #[test]
1185 fn t001_expect_soft_counts_as_assertion() {
1186 let source = fixture("t001_expect_soft.test.ts");
1188 let extractor = TypeScriptExtractor::new();
1189 let funcs = extractor.extract_test_functions(&source, "t001_expect_soft.test.ts");
1190 assert_eq!(funcs.len(), 1);
1191 assert!(
1192 funcs[0].analysis.assertion_count >= 1,
1193 "expect.soft() should count as assertion, got {}",
1194 funcs[0].analysis.assertion_count
1195 );
1196 }
1197
1198 #[test]
1199 fn t001_expect_element_counts_as_assertion() {
1200 let source = fixture("t001_expect_element.test.ts");
1202 let extractor = TypeScriptExtractor::new();
1203 let funcs = extractor.extract_test_functions(&source, "t001_expect_element.test.ts");
1204 assert_eq!(funcs.len(), 1);
1205 assert!(
1206 funcs[0].analysis.assertion_count >= 1,
1207 "expect.element() should count as assertion, got {}",
1208 funcs[0].analysis.assertion_count
1209 );
1210 }
1211
1212 #[test]
1213 fn t001_expect_poll_counts_as_assertion() {
1214 let source = fixture("t001_expect_poll.test.ts");
1216 let extractor = TypeScriptExtractor::new();
1217 let funcs = extractor.extract_test_functions(&source, "t001_expect_poll.test.ts");
1218 assert_eq!(funcs.len(), 1);
1219 assert!(
1220 funcs[0].analysis.assertion_count >= 1,
1221 "expect.poll() should count as assertion, got {}",
1222 funcs[0].analysis.assertion_count
1223 );
1224 }
1225
1226 #[test]
1229 fn t001_chai_property_fixture_all_detected() {
1230 let source = fixture("t001_chai_property.test.ts");
1232 let extractor = TypeScriptExtractor::new();
1233 let funcs = extractor.extract_test_functions(&source, "t001_chai_property.test.ts");
1234 assert_eq!(funcs.len(), 6);
1235 for f in &funcs {
1236 assert!(
1237 f.analysis.assertion_count >= 1,
1238 "test '{}' should have assertion_count >= 1, got {}",
1239 f.name,
1240 f.analysis.assertion_count
1241 );
1242 }
1243 }
1244
1245 #[test]
1246 fn t001_chai_property_depth1_no_double_count() {
1247 let source = r#"
1249import { expect } from 'chai';
1250describe('d', () => {
1251 it('t', () => {
1252 expect(x).ok;
1253 });
1254});
1255"#;
1256 let extractor = TypeScriptExtractor::new();
1257 let funcs = extractor.extract_test_functions(source, "test.ts");
1258 assert_eq!(funcs.len(), 1);
1259 assert_eq!(
1260 funcs[0].analysis.assertion_count, 1,
1261 "depth 1 property should count exactly 1, got {}",
1262 funcs[0].analysis.assertion_count
1263 );
1264 }
1265
1266 #[test]
1267 fn t001_chai_property_depth3_no_double_count() {
1268 let source = r#"
1270import { expect } from 'chai';
1271describe('d', () => {
1272 it('t', () => {
1273 expect(x).to.be.true;
1274 });
1275});
1276"#;
1277 let extractor = TypeScriptExtractor::new();
1278 let funcs = extractor.extract_test_functions(source, "test.ts");
1279 assert_eq!(funcs.len(), 1);
1280 assert_eq!(
1281 funcs[0].analysis.assertion_count, 1,
1282 "depth 3 property should count exactly 1, got {}",
1283 funcs[0].analysis.assertion_count
1284 );
1285 }
1286
1287 #[test]
1288 fn t001_chai_property_depth4_no_double_count() {
1289 let source = r#"
1291import { expect } from 'chai';
1292describe('d', () => {
1293 it('t', () => {
1294 expect(spy).to.have.been.calledOnce;
1295 });
1296});
1297"#;
1298 let extractor = TypeScriptExtractor::new();
1299 let funcs = extractor.extract_test_functions(source, "test.ts");
1300 assert_eq!(funcs.len(), 1);
1301 assert_eq!(
1302 funcs[0].analysis.assertion_count, 1,
1303 "depth 4 property should count exactly 1, got {}",
1304 funcs[0].analysis.assertion_count
1305 );
1306 }
1307
1308 #[test]
1309 fn t001_chai_property_intermediate_not_counted() {
1310 let source = r#"
1313import { expect } from 'chai';
1314describe('d', () => {
1315 it('t', () => {
1316 expect(x).to;
1317 });
1318});
1319"#;
1320 let extractor = TypeScriptExtractor::new();
1321 let funcs = extractor.extract_test_functions(source, "test.ts");
1322 assert_eq!(funcs.len(), 1);
1323 assert_eq!(
1324 funcs[0].analysis.assertion_count, 0,
1325 "intermediate property .to should NOT count as assertion, got {}",
1326 funcs[0].analysis.assertion_count
1327 );
1328 }
1329
1330 #[test]
1333 fn t001_not_modifier_all_detected() {
1334 let source = fixture("t001_not_modifier.test.ts");
1336 let extractor = TypeScriptExtractor::new();
1337 let funcs = extractor.extract_test_functions(&source, "t001_not_modifier.test.ts");
1338 assert_eq!(funcs.len(), 3);
1339 for f in &funcs {
1340 assert_eq!(
1341 f.analysis.assertion_count, 1,
1342 "test '{}' with .not modifier should have assertion_count == 1, got {}",
1343 f.name, f.analysis.assertion_count
1344 );
1345 }
1346 }
1347
1348 #[test]
1349 fn t001_resolves_rejects_chain_all_detected() {
1350 let source = fixture("t001_resolves_rejects_chain.test.ts");
1352 let extractor = TypeScriptExtractor::new();
1353 let funcs =
1354 extractor.extract_test_functions(&source, "t001_resolves_rejects_chain.test.ts");
1355 assert_eq!(funcs.len(), 3);
1356 for f in &funcs {
1357 assert_eq!(
1358 f.analysis.assertion_count, 1,
1359 "test '{}' with modifier chain should have assertion_count == 1, got {}",
1360 f.name, f.analysis.assertion_count
1361 );
1362 }
1363 }
1364
1365 #[test]
1368 fn t001_chai_method_call_fixture_all_detected() {
1369 let source = fixture("t001_chai_method_call.test.ts");
1371 let extractor = TypeScriptExtractor::new();
1372 let funcs = extractor.extract_test_functions(&source, "t001_chai_method_call.test.ts");
1373 assert_eq!(funcs.len(), 18);
1374
1375 assert_eq!(
1377 funcs[0].analysis.assertion_count, 1,
1378 "TC-01 to.equal should count exactly 1, got {}",
1379 funcs[0].analysis.assertion_count
1380 );
1381
1382 assert!(
1384 funcs[1].analysis.assertion_count >= 1,
1385 "TC-02 to.be.a should have assertion_count >= 1, got {}",
1386 funcs[1].analysis.assertion_count
1387 );
1388
1389 assert!(
1391 funcs[2].analysis.assertion_count >= 1,
1392 "TC-03 to.have.callCount should have assertion_count >= 1, got {}",
1393 funcs[2].analysis.assertion_count
1394 );
1395
1396 assert!(
1398 funcs[3].analysis.assertion_count >= 1,
1399 "TC-04 to.have.been.calledWith should have assertion_count >= 1, got {}",
1400 funcs[3].analysis.assertion_count
1401 );
1402
1403 assert!(
1405 funcs[4].analysis.assertion_count >= 1,
1406 "TC-05 to.not.have.been.calledWith should have assertion_count >= 1, got {}",
1407 funcs[4].analysis.assertion_count
1408 );
1409
1410 assert!(
1412 funcs[5].analysis.assertion_count >= 2,
1413 "TC-06 mixed property+method should have assertion_count >= 2, got {}",
1414 funcs[5].analysis.assertion_count
1415 );
1416
1417 assert!(
1419 funcs[6].analysis.assertion_count >= 2,
1420 "TC-07 multiple methods should have assertion_count >= 2, got {}",
1421 funcs[6].analysis.assertion_count
1422 );
1423
1424 assert_eq!(
1426 funcs[7].analysis.assertion_count, 0,
1427 "TC-08 no assertion should have assertion_count == 0, got {}",
1428 funcs[7].analysis.assertion_count
1429 );
1430
1431 assert_eq!(
1433 funcs[8].analysis.assertion_count, 0,
1434 "TC-09 customHelper should have assertion_count == 0, got {}",
1435 funcs[8].analysis.assertion_count
1436 );
1437
1438 assert!(
1440 funcs[9].analysis.assertion_count >= 1,
1441 "TC-10 not.to.equal should have assertion_count >= 1, got {}",
1442 funcs[9].analysis.assertion_count
1443 );
1444
1445 assert_eq!(
1447 funcs[10].analysis.assertion_count, 1,
1448 "TC-11 to.equal regression should count exactly 1, got {}",
1449 funcs[10].analysis.assertion_count
1450 );
1451
1452 assert!(
1454 funcs[11].analysis.assertion_count >= 1,
1455 "TC-12 deep intermediate should have assertion_count >= 1, got {}",
1456 funcs[11].analysis.assertion_count
1457 );
1458
1459 assert!(
1461 funcs[12].analysis.assertion_count >= 1,
1462 "TC-13 nested intermediate should have assertion_count >= 1, got {}",
1463 funcs[12].analysis.assertion_count
1464 );
1465
1466 assert!(
1468 funcs[13].analysis.assertion_count >= 1,
1469 "TC-14 own intermediate should have assertion_count >= 1, got {}",
1470 funcs[13].analysis.assertion_count
1471 );
1472
1473 assert!(
1475 funcs[14].analysis.assertion_count >= 1,
1476 "TC-15 ordered intermediate should have assertion_count >= 1, got {}",
1477 funcs[14].analysis.assertion_count
1478 );
1479
1480 assert!(
1482 funcs[15].analysis.assertion_count >= 1,
1483 "TC-16 any intermediate should have assertion_count >= 1, got {}",
1484 funcs[15].analysis.assertion_count
1485 );
1486
1487 assert!(
1489 funcs[16].analysis.assertion_count >= 1,
1490 "TC-17 all intermediate should have assertion_count >= 1, got {}",
1491 funcs[16].analysis.assertion_count
1492 );
1493
1494 assert!(
1496 funcs[17].analysis.assertion_count >= 1,
1497 "TC-18 itself intermediate should have assertion_count >= 1, got {}",
1498 funcs[17].analysis.assertion_count
1499 );
1500 }
1501
1502 #[test]
1503 fn t001_chai_method_call_depth2_no_double_count() {
1504 let source = r#"
1506import { expect } from 'chai';
1507describe('d', () => {
1508 it('t', () => {
1509 expect(x).to.equal(y);
1510 });
1511});
1512"#;
1513 let extractor = TypeScriptExtractor::new();
1514 let funcs = extractor.extract_test_functions(source, "test.ts");
1515 assert_eq!(funcs.len(), 1);
1516 assert_eq!(
1517 funcs[0].analysis.assertion_count, 1,
1518 "depth 2 method-call should count exactly 1, got {}",
1519 funcs[0].analysis.assertion_count
1520 );
1521 }
1522
1523 #[test]
1524 fn t001_chai_deep_intermediate_no_double_count() {
1525 let source = r#"
1527import { expect } from 'chai';
1528describe('d', () => {
1529 it('t', () => {
1530 expect(obj).to.have.deep.equal({a: 1});
1531 });
1532});
1533"#;
1534 let extractor = TypeScriptExtractor::new();
1535 let funcs = extractor.extract_test_functions(source, "test.ts");
1536 assert_eq!(funcs.len(), 1);
1537 assert_eq!(
1538 funcs[0].analysis.assertion_count, 1,
1539 "deep intermediate should count exactly 1, got {}",
1540 funcs[0].analysis.assertion_count
1541 );
1542 }
1543
1544 #[test]
1545 fn t001_expect_soft_chain_fixture() {
1546 let source = fixture("t001_expect_soft_chain.test.ts");
1548 let extractor = TypeScriptExtractor::new();
1549 let funcs = extractor.extract_test_functions(&source, "t001_expect_soft_chain.test.ts");
1550 assert_eq!(funcs.len(), 10);
1551
1552 assert!(
1554 funcs[0].analysis.assertion_count >= 1,
1555 "B1 expect.soft depth-2 should have assertion_count >= 1, got {}",
1556 funcs[0].analysis.assertion_count
1557 );
1558
1559 assert!(
1561 funcs[1].analysis.assertion_count >= 1,
1562 "B2 expect.soft.not depth-3 should have assertion_count >= 1, got {}",
1563 funcs[1].analysis.assertion_count
1564 );
1565
1566 assert!(
1568 funcs[2].analysis.assertion_count >= 1,
1569 "B3 expect.soft.resolves depth-3 should have assertion_count >= 1, got {}",
1570 funcs[2].analysis.assertion_count
1571 );
1572
1573 assert!(
1575 funcs[3].analysis.assertion_count >= 1,
1576 "B4 expect.soft.rejects depth-3 should have assertion_count >= 1, got {}",
1577 funcs[3].analysis.assertion_count
1578 );
1579
1580 assert!(
1582 funcs[4].analysis.assertion_count >= 1,
1583 "B5 expect.soft.resolves.not depth-4 should have assertion_count >= 1, got {}",
1584 funcs[4].analysis.assertion_count
1585 );
1586
1587 assert!(
1589 funcs[5].analysis.assertion_count >= 1,
1590 "B6 expect.soft.rejects.not depth-4 should have assertion_count >= 1, got {}",
1591 funcs[5].analysis.assertion_count
1592 );
1593
1594 assert_eq!(
1596 funcs[6].analysis.assertion_count, 0,
1597 "B7 customHelper should have assertion_count == 0, got {}",
1598 funcs[6].analysis.assertion_count
1599 );
1600
1601 assert_eq!(
1603 funcs[7].analysis.assertion_count, 0,
1604 "B8 no assertion should have assertion_count == 0, got {}",
1605 funcs[7].analysis.assertion_count
1606 );
1607
1608 assert!(
1610 funcs[8].analysis.assertion_count >= 1,
1611 "B9 expect.element.not depth-3 should have assertion_count >= 1, got {}",
1612 funcs[8].analysis.assertion_count
1613 );
1614
1615 assert!(
1617 funcs[9].analysis.assertion_count >= 1,
1618 "B10 expect.poll.not depth-3 should have assertion_count >= 1, got {}",
1619 funcs[9].analysis.assertion_count
1620 );
1621 }
1622
1623 #[test]
1626 fn t001_supertest_expect_method_call() {
1627 let source = fixture("t001_supertest.test.ts");
1628 let extractor = TypeScriptExtractor::new();
1629 let funcs = extractor.extract_test_functions(&source, "t001_supertest.test.ts");
1630 assert_eq!(funcs.len(), 6);
1631
1632 assert_eq!(
1634 funcs[0].analysis.assertion_count, 1,
1635 "TC-01 single .expect(200) should have assertion_count == 1, got {}",
1636 funcs[0].analysis.assertion_count
1637 );
1638
1639 assert_eq!(
1641 funcs[1].analysis.assertion_count, 2,
1642 "TC-02 two .expect() should have assertion_count == 2, got {}",
1643 funcs[1].analysis.assertion_count
1644 );
1645
1646 assert_eq!(
1648 funcs[2].analysis.assertion_count, 2,
1649 "TC-03 .set() + two .expect() should have assertion_count == 2, got {}",
1650 funcs[2].analysis.assertion_count
1651 );
1652
1653 assert_eq!(
1655 funcs[3].analysis.assertion_count, 0,
1656 "TC-04 no assertion should have assertion_count == 0, got {}",
1657 funcs[3].analysis.assertion_count
1658 );
1659
1660 assert_eq!(
1662 funcs[4].analysis.assertion_count, 1,
1663 "TC-05 standalone expect should have assertion_count == 1, got {}",
1664 funcs[4].analysis.assertion_count
1665 );
1666
1667 assert_eq!(
1669 funcs[5].analysis.assertion_count, 1,
1670 "TC-06 non-supertest builder .expect() should have assertion_count == 1, got {}",
1671 funcs[5].analysis.assertion_count
1672 );
1673 }
1674
1675 #[test]
1678 fn t001_chai_vocab_expansion_fixture_all_detected() {
1679 let source = fixture("t001_chai_vocab_expansion.test.ts");
1680 let extractor = TypeScriptExtractor::new();
1681 let funcs = extractor.extract_test_functions(&source, "t001_chai_vocab_expansion.test.ts");
1682 assert_eq!(funcs.len(), 18);
1683
1684 for (i, f) in funcs.iter().enumerate().take(16) {
1686 assert!(
1687 f.analysis.assertion_count >= 1,
1688 "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1689 i + 1,
1690 f.name,
1691 f.analysis.assertion_count
1692 );
1693 }
1694
1695 assert_eq!(
1697 funcs[16].analysis.assertion_count, 0,
1698 "TC-17 sinon.stub should have assertion_count == 0, got {}",
1699 funcs[16].analysis.assertion_count
1700 );
1701
1702 assert_eq!(
1704 funcs[17].analysis.assertion_count, 0,
1705 "TC-18 no assertion should have assertion_count == 0, got {}",
1706 funcs[17].analysis.assertion_count
1707 );
1708 }
1709
1710 #[test]
1713 fn t001_chai_property_arrow_fixture_all_detected() {
1714 let source = fixture("t001_chai_property_arrow.test.ts");
1715 let extractor = TypeScriptExtractor::new();
1716 let funcs = extractor.extract_test_functions(&source, "t001_chai_property_arrow.test.ts");
1717 assert_eq!(funcs.len(), 8);
1718
1719 for (i, f) in funcs.iter().enumerate().take(7) {
1721 assert!(
1722 f.analysis.assertion_count >= 1,
1723 "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1724 i + 1,
1725 f.name,
1726 f.analysis.assertion_count
1727 );
1728 }
1729
1730 assert_eq!(
1732 funcs[7].analysis.assertion_count, 0,
1733 "TC-08 no assertion should have assertion_count == 0, got {}",
1734 funcs[7].analysis.assertion_count
1735 );
1736 }
1737
1738 #[test]
1741 fn t001_chai_property_return_fixture_detected() {
1742 let source = fixture("t001_chai_property_return.test.ts");
1743 let extractor = TypeScriptExtractor::new();
1744 let funcs = extractor.extract_test_functions(&source, "t001_chai_property_return.test.ts");
1745 assert_eq!(funcs.len(), 6);
1746
1747 for (i, f) in funcs.iter().enumerate().take(5) {
1749 assert_eq!(
1750 f.analysis.assertion_count,
1751 1,
1752 "TC-{:02} '{}' should have assertion_count == 1, got {}",
1753 i + 1,
1754 f.name,
1755 f.analysis.assertion_count
1756 );
1757 }
1758
1759 assert_eq!(
1761 funcs[5].analysis.assertion_count, 0,
1762 "TC-06 '{}' non-assertion return should have assertion_count == 0, got {}",
1763 funcs[5].name, funcs[5].analysis.assertion_count
1764 );
1765 }
1766
1767 #[test]
1768 fn t001_chai_property_existing_wrappers_regression() {
1769 let extractor = TypeScriptExtractor::new();
1770
1771 let expr_source = fixture("t001_chai_property.test.ts");
1773 let expr_funcs =
1774 extractor.extract_test_functions(&expr_source, "t001_chai_property.test.ts");
1775 assert_eq!(expr_funcs[1].name, "should detect to.be.true (depth 3)");
1776 assert_eq!(
1777 expr_funcs[1].analysis.assertion_count, 1,
1778 "expression_statement wrapper regression: expected exactly 1 assertion, got {}",
1779 expr_funcs[1].analysis.assertion_count
1780 );
1781
1782 let arrow_source = fixture("t001_chai_property_arrow.test.ts");
1784 let arrow_funcs =
1785 extractor.extract_test_functions(&arrow_source, "t001_chai_property_arrow.test.ts");
1786 assert_eq!(
1787 arrow_funcs[0].name,
1788 "should detect property in forEach arrow"
1789 );
1790 assert_eq!(
1791 arrow_funcs[0].analysis.assertion_count, 1,
1792 "arrow_function body regression: expected exactly 1 assertion, got {}",
1793 arrow_funcs[0].analysis.assertion_count
1794 );
1795 }
1796
1797 #[test]
1798 fn t107_skipped_for_typescript() {
1799 let source = fixture("t107_pass.test.ts");
1802 let extractor = TypeScriptExtractor::new();
1803 let funcs = extractor.extract_test_functions(&source, "t107_pass.test.ts");
1804 assert_eq!(funcs.len(), 1);
1805 let analysis = &funcs[0].analysis;
1806 assert!(
1807 analysis.assertion_count >= 2,
1808 "fixture should have 2+ assertions: got {}",
1809 analysis.assertion_count
1810 );
1811 assert_eq!(
1812 analysis.assertion_message_count, analysis.assertion_count,
1813 "TS assertion_message_count should equal assertion_count to skip T107"
1814 );
1815 }
1816
1817 #[test]
1819 fn t001_custom_helper_with_config_no_violation() {
1820 use exspec_core::query_utils::apply_custom_assertion_fallback;
1821 use exspec_core::rules::{evaluate_rules, Config};
1822
1823 let source = fixture("t001_custom_helper.test.ts");
1824 let extractor = TypeScriptExtractor::new();
1825 let mut analysis = extractor.extract_file_analysis(&source, "t001_custom_helper.test.ts");
1826 let patterns = vec!["myAssert(".to_string()];
1827 apply_custom_assertion_fallback(&mut analysis, &source, &patterns);
1828
1829 let config = Config::default();
1830 let diags = evaluate_rules(&analysis.functions, &config);
1831 let t001_diags: Vec<_> = diags.iter().filter(|d| d.rule.0 == "T001").collect();
1832 assert_eq!(
1836 t001_diags.len(),
1837 1,
1838 "only 'has no assertion' test should trigger T001"
1839 );
1840 }
1841
1842 #[test]
1845 fn t001_expect_assertions_counts_as_assertion() {
1846 let source = fixture("t001_expect_assertions.test.ts");
1848 let extractor = TypeScriptExtractor::new();
1849 let funcs = extractor.extract_test_functions(&source, "t001_expect_assertions.test.ts");
1850 assert_eq!(funcs.len(), 8);
1852
1853 assert!(
1855 funcs[0].analysis.assertion_count >= 1,
1856 "expect.assertions(N) should count as assertion, got {}",
1857 funcs[0].analysis.assertion_count
1858 );
1859
1860 assert_eq!(
1862 funcs[1].analysis.assertion_count, 1,
1863 "expect.assertions(0) should count as exactly 1 assertion, got {}",
1864 funcs[1].analysis.assertion_count
1865 );
1866
1867 assert!(
1869 funcs[2].analysis.assertion_count >= 1,
1870 "expect.hasAssertions() should count as assertion, got {}",
1871 funcs[2].analysis.assertion_count
1872 );
1873
1874 assert_eq!(
1876 funcs[3].analysis.assertion_count, 1,
1877 "expect.unreachable() should count as exactly 1 assertion, got {}",
1878 funcs[3].analysis.assertion_count
1879 );
1880
1881 assert_eq!(
1883 funcs[4].analysis.assertion_count, 1,
1884 "expectType<T>(value) should count as exactly 1 assertion, got {}",
1885 funcs[4].analysis.assertion_count
1886 );
1887
1888 assert!(
1890 funcs[5].analysis.assertion_count >= 2,
1891 "mixed expect.assertions + expect().toBe() should count 2+, got {}",
1892 funcs[5].analysis.assertion_count
1893 );
1894
1895 assert!(
1897 funcs[6].analysis.assertion_count >= 2,
1898 "expectType + expectTypeOf should count 2+, got {}",
1899 funcs[6].analysis.assertion_count
1900 );
1901
1902 assert_eq!(
1904 funcs[7].analysis.assertion_count, 0,
1905 "no-assertion test should have assertion_count == 0"
1906 );
1907 }
1908
1909 #[test]
1912 fn t001_chai_nestjs_aliases_fixture() {
1913 let source = fixture("t001_chai_nestjs_aliases.test.ts");
1914 let extractor = TypeScriptExtractor::new();
1915 let funcs = extractor.extract_test_functions(&source, "t001_chai_nestjs_aliases.test.ts");
1916 assert_eq!(funcs.len(), 9);
1917
1918 assert!(
1920 funcs[0].analysis.assertion_count >= 1,
1921 "TC-01 instanceof alias should have assertion_count >= 1, got {}",
1922 funcs[0].analysis.assertion_count
1923 );
1924
1925 assert!(
1927 funcs[1].analysis.assertion_count >= 1,
1928 "TC-02 throws alias should have assertion_count >= 1, got {}",
1929 funcs[1].analysis.assertion_count
1930 );
1931
1932 assert!(
1934 funcs[2].analysis.assertion_count >= 1,
1935 "TC-03 contains alias should have assertion_count >= 1, got {}",
1936 funcs[2].analysis.assertion_count
1937 );
1938
1939 assert!(
1941 funcs[3].analysis.assertion_count >= 1,
1942 "TC-04 equals alias should have assertion_count >= 1, got {}",
1943 funcs[3].analysis.assertion_count
1944 );
1945
1946 assert!(
1948 funcs[4].analysis.assertion_count >= 1,
1949 "TC-05 ownProperty should have assertion_count >= 1, got {}",
1950 funcs[4].analysis.assertion_count
1951 );
1952
1953 assert!(
1955 funcs[5].analysis.assertion_count >= 1,
1956 "TC-06 length alias should have assertion_count >= 1, got {}",
1957 funcs[5].analysis.assertion_count
1958 );
1959
1960 assert!(
1962 funcs[6].analysis.assertion_count >= 1,
1963 "TC-07 throw property should have assertion_count >= 1, got {}",
1964 funcs[6].analysis.assertion_count
1965 );
1966
1967 assert!(
1969 funcs[7].analysis.assertion_count >= 1,
1970 "TC-08 and+instanceof deep chain should have assertion_count >= 1, got {}",
1971 funcs[7].analysis.assertion_count
1972 );
1973
1974 assert_eq!(
1976 funcs[8].analysis.assertion_count, 0,
1977 "TC-09 no assertion should have assertion_count == 0, got {}",
1978 funcs[8].analysis.assertion_count
1979 );
1980 }
1981
1982 #[test]
1985 fn t001_sinon_verify_fixture_all_detected() {
1986 let source = fixture("t001_sinon_verify.test.ts");
1987 let extractor = TypeScriptExtractor::new();
1988 let funcs = extractor.extract_test_functions(&source, "t001_sinon_verify.test.ts");
1989 assert_eq!(funcs.len(), 7);
1990
1991 for (i, f) in funcs.iter().enumerate().take(5) {
1993 assert!(
1994 f.analysis.assertion_count >= 1,
1995 "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1996 i + 1,
1997 f.name,
1998 f.analysis.assertion_count
1999 );
2000 }
2001
2002 assert!(
2004 funcs[4].analysis.assertion_count >= 2,
2005 "TC-05 verify + expect should have assertion_count >= 2, got {}",
2006 funcs[4].analysis.assertion_count
2007 );
2008
2009 assert_eq!(
2011 funcs[5].analysis.assertion_count, 0,
2012 "TC-06 mock.restore should have assertion_count == 0, got {}",
2013 funcs[5].analysis.assertion_count
2014 );
2015
2016 assert_eq!(
2018 funcs[6].analysis.assertion_count, 0,
2019 "TC-07 no assertion should have assertion_count == 0, got {}",
2020 funcs[6].analysis.assertion_count
2021 );
2022 }
2023
2024 #[test]
2027 fn helper_tracing_tc01_calls_helper_with_assert() {
2028 let source = fixture("t001_pass_helper_tracing.test.ts");
2030 let extractor = TypeScriptExtractor::new();
2031 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2032 let func = fa
2033 .functions
2034 .iter()
2035 .find(|f| f.name == "TC-01 calls helper with assert")
2036 .expect("TC-01 calls helper with assert not found");
2037 assert!(
2038 func.analysis.assertion_count >= 1,
2039 "TC-01: expected assertion_count >= 1, got {}",
2040 func.analysis.assertion_count
2041 );
2042 }
2043
2044 #[test]
2045 fn helper_tracing_tc02_calls_helper_without_assert() {
2046 let source = fixture("t001_pass_helper_tracing.test.ts");
2048 let extractor = TypeScriptExtractor::new();
2049 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2050 let func = fa
2051 .functions
2052 .iter()
2053 .find(|f| f.name == "TC-02 calls helper without assert")
2054 .expect("TC-02 calls helper without assert not found");
2055 assert_eq!(
2056 func.analysis.assertion_count, 0,
2057 "TC-02: expected assertion_count == 0, got {}",
2058 func.analysis.assertion_count
2059 );
2060 }
2061
2062 #[test]
2063 fn helper_tracing_tc03_has_own_assert_plus_helper() {
2064 let source = fixture("t001_pass_helper_tracing.test.ts");
2066 let extractor = TypeScriptExtractor::new();
2067 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2068 let func = fa
2069 .functions
2070 .iter()
2071 .find(|f| f.name == "TC-03 has own assert plus helper")
2072 .expect("TC-03 has own assert plus helper not found");
2073 assert!(
2074 func.analysis.assertion_count >= 1,
2075 "TC-03: expected assertion_count >= 1, got {}",
2076 func.analysis.assertion_count
2077 );
2078 }
2079
2080 #[test]
2081 fn helper_tracing_tc04_calls_undefined_function() {
2082 let source = fixture("t001_pass_helper_tracing.test.ts");
2084 let extractor = TypeScriptExtractor::new();
2085 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2086 let func = fa
2087 .functions
2088 .iter()
2089 .find(|f| f.name == "TC-04 calls undefined function")
2090 .expect("TC-04 calls undefined function not found");
2091 assert_eq!(
2092 func.analysis.assertion_count, 0,
2093 "TC-04: expected assertion_count == 0, got {}",
2094 func.analysis.assertion_count
2095 );
2096 }
2097
2098 #[test]
2099 fn helper_tracing_tc05_two_hop_tracing() {
2100 let source = fixture("t001_pass_helper_tracing.test.ts");
2103 let extractor = TypeScriptExtractor::new();
2104 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2105 let func = fa
2106 .functions
2107 .iter()
2108 .find(|f| f.name == "TC-05 two hop tracing")
2109 .expect("TC-05 two hop tracing not found");
2110 assert_eq!(
2111 func.analysis.assertion_count, 0,
2112 "TC-05: 2-hop not traced, expected assertion_count == 0, got {}",
2113 func.analysis.assertion_count
2114 );
2115 }
2116
2117 #[test]
2118 fn helper_tracing_tc06_with_assertion_early_return() {
2119 let source = fixture("t001_pass_helper_tracing.test.ts");
2121 let extractor = TypeScriptExtractor::new();
2122 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2123 let func = fa
2124 .functions
2125 .iter()
2126 .find(|f| f.name == "TC-06 with assertion early return")
2127 .expect("TC-06 with assertion early return not found");
2128 assert!(
2129 func.analysis.assertion_count >= 1,
2130 "TC-06: expected assertion_count >= 1, got {}",
2131 func.analysis.assertion_count
2132 );
2133 }
2134
2135 #[test]
2136 fn helper_tracing_tc07_multiple_calls_same_helper() {
2137 let source = fixture("t001_pass_helper_tracing.test.ts");
2141 let extractor = TypeScriptExtractor::new();
2142 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2143 let func = fa
2144 .functions
2145 .iter()
2146 .find(|f| f.name == "TC-07 multiple calls same helper")
2147 .expect("TC-07 multiple calls same helper not found");
2148 assert_eq!(
2149 func.analysis.assertion_count, 1,
2150 "TC-07: dedup expected assertion_count == 1, got {}",
2151 func.analysis.assertion_count
2152 );
2153 }
2154
2155 #[test]
2156 fn helper_tracing_tc08_arrow_function_helper() {
2157 let source = fixture("t001_pass_helper_tracing.test.ts");
2159 let extractor = TypeScriptExtractor::new();
2160 let fa = extractor.extract_file_analysis(&source, "t001_pass_helper_tracing.test.ts");
2161 let func = fa
2162 .functions
2163 .iter()
2164 .find(|f| f.name == "TC-08 arrow function helper")
2165 .expect("TC-08 arrow function helper not found");
2166 assert!(
2167 func.analysis.assertion_count >= 1,
2168 "TC-08: arrow function helper expected assertion_count >= 1, got {}",
2169 func.analysis.assertion_count
2170 );
2171 }
2172}