1use std::sync::OnceLock;
2
3use exspec_core::extractor::{FileAnalysis, LanguageExtractor, TestAnalysis, TestFunction};
4use exspec_core::query_utils::{
5 collect_mock_class_names, count_captures, count_captures_within_context,
6 count_duplicate_literals, extract_suppression_from_previous_line, has_any_match,
7};
8use streaming_iterator::StreamingIterator;
9use tree_sitter::{Node, Parser, Query, QueryCursor};
10
11const TEST_FUNCTION_QUERY: &str = include_str!("../queries/test_function.scm");
12const ASSERTION_QUERY: &str = include_str!("../queries/assertion.scm");
13const MOCK_USAGE_QUERY: &str = include_str!("../queries/mock_usage.scm");
14const MOCK_ASSIGNMENT_QUERY: &str = include_str!("../queries/mock_assignment.scm");
15const PARAMETERIZED_QUERY: &str = include_str!("../queries/parameterized.scm");
16const IMPORT_PBT_QUERY: &str = include_str!("../queries/import_pbt.scm");
17const IMPORT_CONTRACT_QUERY: &str = include_str!("../queries/import_contract.scm");
18const HOW_NOT_WHAT_QUERY: &str = include_str!("../queries/how_not_what.scm");
19const PRIVATE_IN_ASSERTION_QUERY: &str = include_str!("../queries/private_in_assertion.scm");
20const ERROR_TEST_QUERY: &str = include_str!("../queries/error_test.scm");
21const RELATIONAL_ASSERTION_QUERY: &str = include_str!("../queries/relational_assertion.scm");
22const WAIT_AND_SEE_QUERY: &str = include_str!("../queries/wait_and_see.scm");
23
24fn ts_language() -> tree_sitter::Language {
25 tree_sitter_typescript::LANGUAGE_TSX.into()
26}
27
28fn cached_query<'a>(lock: &'a OnceLock<Query>, source: &str) -> &'a Query {
29 lock.get_or_init(|| Query::new(&ts_language(), source).expect("invalid query"))
30}
31
32static TEST_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
33static ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
34static MOCK_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
35static MOCK_ASSIGN_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
36static PARAMETERIZED_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
37static IMPORT_PBT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
38static IMPORT_CONTRACT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
39static HOW_NOT_WHAT_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
40static PRIVATE_IN_ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
41static ERROR_TEST_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
42static RELATIONAL_ASSERTION_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
43static WAIT_AND_SEE_QUERY_CACHE: OnceLock<Query> = OnceLock::new();
44
45pub struct TypeScriptExtractor;
46
47impl TypeScriptExtractor {
48 pub fn new() -> Self {
49 Self
50 }
51
52 pub fn parser() -> Parser {
53 let mut parser = Parser::new();
54 let language = tree_sitter_typescript::LANGUAGE_TSX;
55 parser
56 .set_language(&language.into())
57 .expect("failed to load TypeScript grammar");
58 parser
59 }
60}
61
62impl Default for TypeScriptExtractor {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68fn count_enclosing_describe_fixtures(root: Node, test_start_byte: usize, source: &[u8]) -> usize {
73 let Some(start_node) = root.descendant_for_byte_range(test_start_byte, test_start_byte) else {
74 return 0;
75 };
76
77 let mut count = 0;
78 let mut current = start_node.parent();
79 while let Some(node) = current {
80 if node.kind() == "statement_block" && is_describe_callback_body(node, source) {
82 let child_count = node.named_child_count();
84 for i in 0..child_count {
85 if let Some(child) = node.named_child(i) {
86 let kind = child.kind();
87 if kind == "lexical_declaration" || kind == "variable_declaration" {
88 let declarator_count = (0..child.named_child_count())
91 .filter_map(|j| child.named_child(j))
92 .filter(|c| c.kind() == "variable_declarator")
93 .count();
94 count += declarator_count;
95 }
96 }
97 }
98 }
99 current = node.parent();
100 }
101
102 count
103}
104
105fn is_describe_callback_body(block: Node, source: &[u8]) -> bool {
108 let parent = match block.parent() {
109 Some(p) => p,
110 None => return false,
111 };
112 let kind = parent.kind();
113 if kind != "arrow_function" && kind != "function_expression" {
114 return false;
115 }
116 let args = match parent.parent() {
117 Some(p) if p.kind() == "arguments" => p,
118 _ => return false,
119 };
120 let call = match args.parent() {
121 Some(p) if p.kind() == "call_expression" => p,
122 _ => return false,
123 };
124 if let Some(func_node) = call.child_by_field_name("function") {
126 if let Ok(name) = func_node.utf8_text(source) {
127 return name == "describe" || name.starts_with("describe.");
128 }
129 }
130 false
131}
132
133fn extract_mock_class_name(var_name: &str) -> String {
134 if let Some(stripped) = var_name.strip_prefix("mock") {
136 if !stripped.is_empty() && stripped.starts_with(|c: char| c.is_uppercase()) {
137 return stripped.to_string();
138 }
139 }
140 var_name.to_string()
141}
142
143struct TestMatch {
144 name: String,
145 fn_start_byte: usize,
146 fn_end_byte: usize,
147 fn_start_row: usize,
148 fn_end_row: usize,
149}
150
151fn extract_functions_from_tree(source: &str, file_path: &str, root: Node) -> Vec<TestFunction> {
152 let test_query = cached_query(&TEST_QUERY_CACHE, TEST_FUNCTION_QUERY);
153 let assertion_query = cached_query(&ASSERTION_QUERY_CACHE, ASSERTION_QUERY);
154 let mock_query = cached_query(&MOCK_QUERY_CACHE, MOCK_USAGE_QUERY);
155 let mock_assign_query = cached_query(&MOCK_ASSIGN_QUERY_CACHE, MOCK_ASSIGNMENT_QUERY);
156 let how_not_what_query = cached_query(&HOW_NOT_WHAT_QUERY_CACHE, HOW_NOT_WHAT_QUERY);
157 let private_query = cached_query(
158 &PRIVATE_IN_ASSERTION_QUERY_CACHE,
159 PRIVATE_IN_ASSERTION_QUERY,
160 );
161 let wait_query = cached_query(&WAIT_AND_SEE_QUERY_CACHE, WAIT_AND_SEE_QUERY);
162
163 let name_idx = test_query
164 .capture_index_for_name("name")
165 .expect("no @name capture");
166 let function_idx = test_query
167 .capture_index_for_name("function")
168 .expect("no @function capture");
169
170 let source_bytes = source.as_bytes();
171
172 let mut test_matches = Vec::new();
173 {
174 let mut cursor = QueryCursor::new();
175 let mut matches = cursor.matches(test_query, root, source_bytes);
176 while let Some(m) = matches.next() {
177 let name_capture = match m.captures.iter().find(|c| c.index == name_idx) {
178 Some(c) => c,
179 None => continue,
180 };
181 let name = match name_capture.node.utf8_text(source_bytes) {
182 Ok(s) => s.to_string(),
183 Err(_) => continue,
184 };
185
186 let fn_capture = match m.captures.iter().find(|c| c.index == function_idx) {
187 Some(c) => c,
188 None => continue,
189 };
190
191 test_matches.push(TestMatch {
192 name,
193 fn_start_byte: fn_capture.node.start_byte(),
194 fn_end_byte: fn_capture.node.end_byte(),
195 fn_start_row: fn_capture.node.start_position().row,
196 fn_end_row: fn_capture.node.end_position().row,
197 });
198 }
199 }
200
201 let mut functions = Vec::new();
202 for tm in &test_matches {
203 let fn_node = match root.descendant_for_byte_range(tm.fn_start_byte, tm.fn_end_byte) {
204 Some(n) => n,
205 None => continue,
206 };
207
208 let line = tm.fn_start_row + 1;
209 let end_line = tm.fn_end_row + 1;
210 let line_count = end_line - line + 1;
211
212 let assertion_count = count_captures(assertion_query, "assertion", fn_node, source_bytes);
213 let mock_count = count_captures(mock_query, "mock", fn_node, source_bytes);
214 let mock_classes = collect_mock_class_names(
215 mock_assign_query,
216 fn_node,
217 source_bytes,
218 extract_mock_class_name,
219 );
220
221 let how_not_what_count =
222 count_captures(how_not_what_query, "how_pattern", fn_node, source_bytes);
223
224 let private_in_assertion_count = count_captures_within_context(
225 assertion_query,
226 "assertion",
227 private_query,
228 "private_access",
229 fn_node,
230 source_bytes,
231 );
232
233 let fixture_count = count_enclosing_describe_fixtures(root, tm.fn_start_byte, source_bytes);
234
235 let has_wait = has_any_match(wait_query, "wait", fn_node, source_bytes);
237
238 let duplicate_literal_count = count_duplicate_literals(
240 assertion_query,
241 fn_node,
242 source_bytes,
243 &["number", "string"],
244 );
245
246 let suppressed_rules = extract_suppression_from_previous_line(source, tm.fn_start_row);
247
248 functions.push(TestFunction {
249 name: tm.name.clone(),
250 file: file_path.to_string(),
251 line,
252 end_line,
253 analysis: TestAnalysis {
254 assertion_count,
255 mock_count,
256 mock_classes,
257 line_count,
258 how_not_what_count: how_not_what_count + private_in_assertion_count,
259 fixture_count,
260 has_wait,
261 assertion_message_count: assertion_count, duplicate_literal_count,
263 suppressed_rules,
264 },
265 });
266 }
267
268 functions
269}
270
271impl LanguageExtractor for TypeScriptExtractor {
272 fn extract_test_functions(&self, source: &str, file_path: &str) -> Vec<TestFunction> {
273 let mut parser = Self::parser();
274 let tree = match parser.parse(source, None) {
275 Some(t) => t,
276 None => return Vec::new(),
277 };
278 extract_functions_from_tree(source, file_path, tree.root_node())
279 }
280
281 fn extract_file_analysis(&self, source: &str, file_path: &str) -> FileAnalysis {
282 let mut parser = Self::parser();
283 let tree = match parser.parse(source, None) {
284 Some(t) => t,
285 None => {
286 return FileAnalysis {
287 file: file_path.to_string(),
288 functions: Vec::new(),
289 has_pbt_import: false,
290 has_contract_import: false,
291 has_error_test: false,
292 has_relational_assertion: false,
293 parameterized_count: 0,
294 };
295 }
296 };
297
298 let root = tree.root_node();
299 let source_bytes = source.as_bytes();
300
301 let functions = extract_functions_from_tree(source, file_path, root);
302
303 let param_query = cached_query(&PARAMETERIZED_QUERY_CACHE, PARAMETERIZED_QUERY);
304 let parameterized_count = count_captures(param_query, "parameterized", root, source_bytes);
305
306 let pbt_query = cached_query(&IMPORT_PBT_QUERY_CACHE, IMPORT_PBT_QUERY);
307 let has_pbt_import = has_any_match(pbt_query, "pbt_import", root, source_bytes);
308
309 let contract_query = cached_query(&IMPORT_CONTRACT_QUERY_CACHE, IMPORT_CONTRACT_QUERY);
310 let has_contract_import =
311 has_any_match(contract_query, "contract_import", root, source_bytes);
312
313 let error_test_query = cached_query(&ERROR_TEST_QUERY_CACHE, ERROR_TEST_QUERY);
314 let has_error_test = has_any_match(error_test_query, "error_test", root, source_bytes);
315
316 let relational_query = cached_query(
317 &RELATIONAL_ASSERTION_QUERY_CACHE,
318 RELATIONAL_ASSERTION_QUERY,
319 );
320 let has_relational_assertion =
321 has_any_match(relational_query, "relational", root, source_bytes);
322
323 FileAnalysis {
324 file: file_path.to_string(),
325 functions,
326 has_pbt_import,
327 has_contract_import,
328 has_error_test,
329 has_relational_assertion,
330 parameterized_count,
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn fixture(name: &str) -> String {
340 let path = format!(
341 "{}/tests/fixtures/typescript/{}",
342 env!("CARGO_MANIFEST_DIR").replace("/crates/lang-typescript", ""),
343 name
344 );
345 std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"))
346 }
347
348 #[test]
351 fn parse_typescript_source() {
352 let source = "const x: number = 42;\n";
353 let mut parser = TypeScriptExtractor::parser();
354 let tree = parser.parse(source, None).unwrap();
355 assert_eq!(tree.root_node().kind(), "program");
356 }
357
358 #[test]
359 fn typescript_extractor_implements_language_extractor() {
360 let extractor = TypeScriptExtractor::new();
361 let _: &dyn exspec_core::extractor::LanguageExtractor = &extractor;
362 }
363
364 #[test]
367 fn extract_single_test_function() {
368 let source = fixture("t001_pass.test.ts");
369 let extractor = TypeScriptExtractor::new();
370 let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
371 assert_eq!(funcs.len(), 1);
372 assert_eq!(funcs[0].name, "create user");
373 assert_eq!(funcs[0].line, 1);
374 }
375
376 #[test]
377 fn extract_multiple_tests_excludes_helpers_and_describe() {
378 let source = fixture("multiple_tests.test.ts");
379 let extractor = TypeScriptExtractor::new();
380 let funcs = extractor.extract_test_functions(&source, "multiple_tests.test.ts");
381 assert_eq!(funcs.len(), 3);
382 let names: Vec<&str> = funcs.iter().map(|f| f.name.as_str()).collect();
383 assert_eq!(
384 names,
385 vec!["adds numbers", "subtracts numbers", "multiplies numbers"]
386 );
387 }
388
389 #[test]
390 fn line_count_calculation() {
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!(
395 funcs[0].analysis.line_count,
396 funcs[0].end_line - funcs[0].line + 1
397 );
398 }
399
400 #[test]
401 fn violation_file_extracts_function() {
402 let source = fixture("t001_violation.test.ts");
403 let extractor = TypeScriptExtractor::new();
404 let funcs = extractor.extract_test_functions(&source, "t001_violation.test.ts");
405 assert_eq!(funcs.len(), 1);
406 assert_eq!(funcs[0].name, "create user");
407 }
408
409 #[test]
412 fn assertion_count_zero_for_violation() {
413 let source = fixture("t001_violation.test.ts");
414 let extractor = TypeScriptExtractor::new();
415 let funcs = extractor.extract_test_functions(&source, "t001_violation.test.ts");
416 assert_eq!(funcs[0].analysis.assertion_count, 0);
417 }
418
419 #[test]
420 fn assertion_count_positive_for_pass() {
421 let source = fixture("t001_pass.test.ts");
422 let extractor = TypeScriptExtractor::new();
423 let funcs = extractor.extract_test_functions(&source, "t001_pass.test.ts");
424 assert!(funcs[0].analysis.assertion_count >= 1);
425 }
426
427 #[test]
430 fn tsx_file_detects_assertions() {
431 let source = fixture("t001_tsx_assertion.test.tsx");
432 let extractor = TypeScriptExtractor::new();
433 let funcs = extractor.extract_test_functions(&source, "t001_tsx_assertion.test.tsx");
434 assert_eq!(
435 funcs.len(),
436 2,
437 "should extract 2 test functions from TSX file"
438 );
439 for f in &funcs {
440 assert!(
441 f.analysis.assertion_count >= 1,
442 "test '{}' should have assertions detected in TSX file, got {}",
443 f.name,
444 f.analysis.assertion_count
445 );
446 }
447 }
448
449 #[test]
452 fn mock_count_for_violation() {
453 let source = fixture("t002_violation.test.ts");
454 let extractor = TypeScriptExtractor::new();
455 let funcs = extractor.extract_test_functions(&source, "t002_violation.test.ts");
456 assert_eq!(funcs.len(), 1);
457 assert_eq!(funcs[0].analysis.mock_count, 6);
458 }
459
460 #[test]
461 fn mock_count_for_pass() {
462 let source = fixture("t002_pass.test.ts");
463 let extractor = TypeScriptExtractor::new();
464 let funcs = extractor.extract_test_functions(&source, "t002_pass.test.ts");
465 assert_eq!(funcs.len(), 1);
466 assert_eq!(funcs[0].analysis.mock_count, 1);
467 assert_eq!(funcs[0].analysis.mock_classes, vec!["Db"]);
468 }
469
470 #[test]
471 fn mock_class_name_extraction() {
472 assert_eq!(extract_mock_class_name("mockDb"), "Db");
473 assert_eq!(
474 extract_mock_class_name("mockPaymentService"),
475 "PaymentService"
476 );
477 assert_eq!(extract_mock_class_name("myMock"), "myMock");
478 }
479
480 #[test]
483 fn suppressed_test_has_suppressed_rules() {
484 let source = fixture("suppressed.test.ts");
485 let extractor = TypeScriptExtractor::new();
486 let funcs = extractor.extract_test_functions(&source, "suppressed.test.ts");
487 assert_eq!(funcs.len(), 1);
488 assert_eq!(funcs[0].analysis.mock_count, 6);
489 assert!(funcs[0]
490 .analysis
491 .suppressed_rules
492 .iter()
493 .any(|r| r.0 == "T002"));
494 }
495
496 #[test]
497 fn non_suppressed_test_has_empty_suppressed_rules() {
498 let source = fixture("t002_violation.test.ts");
499 let extractor = TypeScriptExtractor::new();
500 let funcs = extractor.extract_test_functions(&source, "t002_violation.test.ts");
501 assert!(funcs[0].analysis.suppressed_rules.is_empty());
502 }
503
504 #[test]
507 fn giant_test_line_count() {
508 let source = fixture("t003_violation.test.ts");
509 let extractor = TypeScriptExtractor::new();
510 let funcs = extractor.extract_test_functions(&source, "t003_violation.test.ts");
511 assert_eq!(funcs.len(), 1);
512 assert!(funcs[0].analysis.line_count > 50);
513 }
514
515 #[test]
516 fn short_test_line_count() {
517 let source = fixture("t003_pass.test.ts");
518 let extractor = TypeScriptExtractor::new();
519 let funcs = extractor.extract_test_functions(&source, "t003_pass.test.ts");
520 assert_eq!(funcs.len(), 1);
521 assert!(funcs[0].analysis.line_count <= 50);
522 }
523
524 #[test]
527 fn file_analysis_detects_parameterized() {
528 let source = fixture("t004_pass.test.ts");
529 let extractor = TypeScriptExtractor::new();
530 let fa = extractor.extract_file_analysis(&source, "t004_pass.test.ts");
531 assert!(
532 fa.parameterized_count >= 1,
533 "expected parameterized_count >= 1, got {}",
534 fa.parameterized_count
535 );
536 }
537
538 #[test]
539 fn file_analysis_no_parameterized() {
540 let source = fixture("t004_violation.test.ts");
541 let extractor = TypeScriptExtractor::new();
542 let fa = extractor.extract_file_analysis(&source, "t004_violation.test.ts");
543 assert_eq!(fa.parameterized_count, 0);
544 }
545
546 #[test]
549 fn file_analysis_detects_pbt_import() {
550 let source = fixture("t005_pass.test.ts");
551 let extractor = TypeScriptExtractor::new();
552 let fa = extractor.extract_file_analysis(&source, "t005_pass.test.ts");
553 assert!(fa.has_pbt_import);
554 }
555
556 #[test]
557 fn file_analysis_no_pbt_import() {
558 let source = fixture("t005_violation.test.ts");
559 let extractor = TypeScriptExtractor::new();
560 let fa = extractor.extract_file_analysis(&source, "t005_violation.test.ts");
561 assert!(!fa.has_pbt_import);
562 }
563
564 #[test]
567 fn file_analysis_detects_contract_import() {
568 let source = fixture("t008_pass.test.ts");
569 let extractor = TypeScriptExtractor::new();
570 let fa = extractor.extract_file_analysis(&source, "t008_pass.test.ts");
571 assert!(fa.has_contract_import);
572 }
573
574 #[test]
575 fn file_analysis_no_contract_import() {
576 let source = fixture("t008_violation.test.ts");
577 let extractor = TypeScriptExtractor::new();
578 let fa = extractor.extract_file_analysis(&source, "t008_violation.test.ts");
579 assert!(!fa.has_contract_import);
580 }
581
582 #[test]
585 fn suppression_on_describe_does_not_apply_to_inner_tests() {
586 let source = fixture("describe_suppression.test.ts");
587 let extractor = TypeScriptExtractor::new();
588 let funcs = extractor.extract_test_functions(&source, "describe_suppression.test.ts");
589 assert_eq!(funcs.len(), 2, "expected 2 test functions inside describe");
590 for f in &funcs {
591 assert!(
592 f.analysis.suppressed_rules.is_empty(),
593 "test '{}' should NOT have suppressed rules (suppression on describe does not propagate)",
594 f.name
595 );
596 assert_eq!(
597 f.analysis.assertion_count, 0,
598 "test '{}' should have 0 assertions (T001 violation expected)",
599 f.name
600 );
601 }
602 }
603
604 #[test]
607 fn file_analysis_preserves_test_functions() {
608 let source = fixture("t001_pass.test.ts");
609 let extractor = TypeScriptExtractor::new();
610 let fa = extractor.extract_file_analysis(&source, "t001_pass.test.ts");
611 assert_eq!(fa.functions.len(), 1);
612 assert_eq!(fa.functions[0].name, "create user");
613 }
614
615 #[test]
618 fn how_not_what_count_for_violation() {
619 let source = fixture("t101_violation.test.ts");
620 let extractor = TypeScriptExtractor::new();
621 let funcs = extractor.extract_test_functions(&source, "t101_violation.test.ts");
622 assert_eq!(funcs.len(), 2);
623 assert!(
624 funcs[0].analysis.how_not_what_count > 0,
625 "expected how_not_what_count > 0 for first test, got {}",
626 funcs[0].analysis.how_not_what_count
627 );
628 assert!(
629 funcs[1].analysis.how_not_what_count > 0,
630 "expected how_not_what_count > 0 for second test, got {}",
631 funcs[1].analysis.how_not_what_count
632 );
633 }
634
635 #[test]
636 fn how_not_what_count_zero_for_pass() {
637 let source = fixture("t101_pass.test.ts");
638 let extractor = TypeScriptExtractor::new();
639 let funcs = extractor.extract_test_functions(&source, "t101_pass.test.ts");
640 assert_eq!(funcs.len(), 1);
641 assert_eq!(funcs[0].analysis.how_not_what_count, 0);
642 }
643
644 #[test]
645 fn how_not_what_coexists_with_assertions() {
646 let source = fixture("t101_violation.test.ts");
647 let extractor = TypeScriptExtractor::new();
648 let funcs = extractor.extract_test_functions(&source, "t101_violation.test.ts");
649 assert!(
650 funcs[0].analysis.assertion_count > 0,
651 "should also count as assertions"
652 );
653 assert!(
654 funcs[0].analysis.how_not_what_count > 0,
655 "should count as how-not-what"
656 );
657 }
658
659 fn make_query(scm: &str) -> Query {
662 Query::new(&ts_language(), scm).unwrap()
663 }
664
665 #[test]
666 fn query_capture_names_test_function() {
667 let q = make_query(include_str!("../queries/test_function.scm"));
668 assert!(
669 q.capture_index_for_name("name").is_some(),
670 "test_function.scm must define @name capture"
671 );
672 assert!(
673 q.capture_index_for_name("function").is_some(),
674 "test_function.scm must define @function capture"
675 );
676 }
677
678 #[test]
679 fn query_capture_names_assertion() {
680 let q = make_query(include_str!("../queries/assertion.scm"));
681 assert!(
682 q.capture_index_for_name("assertion").is_some(),
683 "assertion.scm must define @assertion capture"
684 );
685 }
686
687 #[test]
688 fn query_capture_names_mock_usage() {
689 let q = make_query(include_str!("../queries/mock_usage.scm"));
690 assert!(
691 q.capture_index_for_name("mock").is_some(),
692 "mock_usage.scm must define @mock capture"
693 );
694 }
695
696 #[test]
697 fn query_capture_names_mock_assignment() {
698 let q = make_query(include_str!("../queries/mock_assignment.scm"));
699 assert!(
700 q.capture_index_for_name("var_name").is_some(),
701 "mock_assignment.scm must define @var_name (required by collect_mock_class_names .expect())"
702 );
703 }
704
705 #[test]
706 fn query_capture_names_parameterized() {
707 let q = make_query(include_str!("../queries/parameterized.scm"));
708 assert!(
709 q.capture_index_for_name("parameterized").is_some(),
710 "parameterized.scm must define @parameterized capture"
711 );
712 }
713
714 #[test]
715 fn query_capture_names_import_pbt() {
716 let q = make_query(include_str!("../queries/import_pbt.scm"));
717 assert!(
718 q.capture_index_for_name("pbt_import").is_some(),
719 "import_pbt.scm must define @pbt_import capture"
720 );
721 }
722
723 #[test]
724 fn query_capture_names_import_contract() {
725 let q = make_query(include_str!("../queries/import_contract.scm"));
726 assert!(
727 q.capture_index_for_name("contract_import").is_some(),
728 "import_contract.scm must define @contract_import capture"
729 );
730 }
731
732 #[test]
733 fn query_capture_names_how_not_what() {
734 let q = make_query(include_str!("../queries/how_not_what.scm"));
735 assert!(
736 q.capture_index_for_name("how_pattern").is_some(),
737 "how_not_what.scm must define @how_pattern capture"
738 );
739 }
740
741 #[test]
744 fn fixture_count_for_violation() {
745 let source = fixture("t102_violation.test.ts");
746 let extractor = TypeScriptExtractor::new();
747 let funcs = extractor.extract_test_functions(&source, "t102_violation.test.ts");
748 assert_eq!(funcs.len(), 1);
749 assert_eq!(
750 funcs[0].analysis.fixture_count, 6,
751 "expected 6 describe-level let declarations"
752 );
753 }
754
755 #[test]
756 fn fixture_count_for_pass() {
757 let source = fixture("t102_pass.test.ts");
758 let extractor = TypeScriptExtractor::new();
759 let funcs = extractor.extract_test_functions(&source, "t102_pass.test.ts");
760 assert_eq!(funcs.len(), 1);
761 assert_eq!(
762 funcs[0].analysis.fixture_count, 2,
763 "expected 2 describe-level let declarations"
764 );
765 }
766
767 #[test]
768 fn fixture_count_nested_describe() {
769 let source = fixture("t102_nested.test.ts");
770 let extractor = TypeScriptExtractor::new();
771 let funcs = extractor.extract_test_functions(&source, "t102_nested.test.ts");
772 assert_eq!(funcs.len(), 2);
773 let inner = funcs
775 .iter()
776 .find(|f| f.name == "test in nested describe inherits all fixtures")
777 .unwrap();
778 assert_eq!(
779 inner.analysis.fixture_count, 6,
780 "inner test should see outer + inner fixtures"
781 );
782 let outer = funcs
784 .iter()
785 .find(|f| f.name == "test in outer describe only sees outer fixtures")
786 .unwrap();
787 assert_eq!(
788 outer.analysis.fixture_count, 3,
789 "outer test should see only outer fixtures"
790 );
791 }
792
793 #[test]
794 fn fixture_count_describe_each() {
795 let source = fixture("t102_describe_each.test.ts");
796 let extractor = TypeScriptExtractor::new();
797 let funcs = extractor.extract_test_functions(&source, "t102_describe_each.test.ts");
798 assert_eq!(funcs.len(), 1);
799 assert_eq!(
800 funcs[0].analysis.fixture_count, 2,
801 "describe.each should be recognized as describe scope"
802 );
803 }
804
805 #[test]
806 fn fixture_count_top_level_test_zero() {
807 let source = "it('standalone test', () => { expect(1).toBe(1); });";
809 let extractor = TypeScriptExtractor::new();
810 let funcs = extractor.extract_test_functions(source, "top_level.test.ts");
811 assert_eq!(funcs.len(), 1);
812 assert_eq!(
813 funcs[0].analysis.fixture_count, 0,
814 "top-level test should have 0 fixtures"
815 );
816 }
817
818 #[test]
821 fn private_dot_notation_detected() {
822 let source = fixture("t101_private_violation.test.ts");
823 let extractor = TypeScriptExtractor::new();
824 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
825 let func = funcs
827 .iter()
828 .find(|f| f.name == "checks internal count via dot notation")
829 .unwrap();
830 assert!(
831 func.analysis.how_not_what_count >= 2,
832 "expected >= 2 private access in assertions (dot), got {}",
833 func.analysis.how_not_what_count
834 );
835 }
836
837 #[test]
838 fn private_bracket_notation_detected() {
839 let source = fixture("t101_private_violation.test.ts");
840 let extractor = TypeScriptExtractor::new();
841 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
842 let func = funcs
844 .iter()
845 .find(|f| f.name == "checks internal via bracket notation")
846 .unwrap();
847 assert!(
848 func.analysis.how_not_what_count >= 2,
849 "expected >= 2 private access in assertions (bracket), got {}",
850 func.analysis.how_not_what_count
851 );
852 }
853
854 #[test]
855 fn private_outside_expect_not_counted() {
856 let source = fixture("t101_private_violation.test.ts");
857 let extractor = TypeScriptExtractor::new();
858 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
859 let func = funcs
861 .iter()
862 .find(|f| f.name == "private outside expect not counted")
863 .unwrap();
864 assert_eq!(
865 func.analysis.how_not_what_count, 0,
866 "private access outside expect should not count"
867 );
868 }
869
870 #[test]
871 fn private_adds_to_how_not_what() {
872 let source = fixture("t101_private_violation.test.ts");
873 let extractor = TypeScriptExtractor::new();
874 let funcs = extractor.extract_test_functions(&source, "t101_private_violation.test.ts");
875 let func = funcs
877 .iter()
878 .find(|f| f.name == "mixed private and mock verification")
879 .unwrap();
880 assert!(
881 func.analysis.how_not_what_count >= 2,
882 "expected mock (1) + private (1) = >= 2, got {}",
883 func.analysis.how_not_what_count
884 );
885 }
886
887 #[test]
888 fn query_capture_names_private_in_assertion() {
889 let q = make_query(include_str!("../queries/private_in_assertion.scm"));
890 assert!(
891 q.capture_index_for_name("private_access").is_some(),
892 "private_in_assertion.scm must define @private_access capture"
893 );
894 }
895
896 #[test]
899 fn error_test_to_throw() {
900 let source = fixture("t103_pass_toThrow.test.ts");
901 let extractor = TypeScriptExtractor::new();
902 let fa = extractor.extract_file_analysis(&source, "t103_pass_toThrow.test.ts");
903 assert!(fa.has_error_test, ".toThrow() should set has_error_test");
904 }
905
906 #[test]
907 fn error_test_to_throw_error() {
908 let source = fixture("t103_pass_toThrowError.test.ts");
909 let extractor = TypeScriptExtractor::new();
910 let fa = extractor.extract_file_analysis(&source, "t103_pass_toThrowError.test.ts");
911 assert!(
912 fa.has_error_test,
913 ".toThrowError() should set has_error_test"
914 );
915 }
916
917 #[test]
918 fn error_test_rejects() {
919 let source = fixture("t103_pass_rejects.test.ts");
920 let extractor = TypeScriptExtractor::new();
921 let fa = extractor.extract_file_analysis(&source, "t103_pass_rejects.test.ts");
922 assert!(fa.has_error_test, ".rejects should set has_error_test");
923 }
924
925 #[test]
926 fn error_test_false_positive_rejects_property() {
927 let source = fixture("t103_false_positive_rejects_property.test.ts");
928 let extractor = TypeScriptExtractor::new();
929 let fa = extractor
930 .extract_file_analysis(&source, "t103_false_positive_rejects_property.test.ts");
931 assert!(
932 !fa.has_error_test,
933 "service.rejects should NOT set has_error_test"
934 );
935 }
936
937 #[test]
938 fn error_test_no_patterns() {
939 let source = fixture("t103_violation.test.ts");
940 let extractor = TypeScriptExtractor::new();
941 let fa = extractor.extract_file_analysis(&source, "t103_violation.test.ts");
942 assert!(
943 !fa.has_error_test,
944 "no error patterns should set has_error_test=false"
945 );
946 }
947
948 #[test]
949 fn query_capture_names_error_test() {
950 let q = make_query(include_str!("../queries/error_test.scm"));
951 assert!(
952 q.capture_index_for_name("error_test").is_some(),
953 "error_test.scm must define @error_test capture"
954 );
955 }
956
957 #[test]
960 fn relational_assertion_violation() {
961 let source = fixture("t105_violation.test.ts");
962 let extractor = TypeScriptExtractor::new();
963 let fa = extractor.extract_file_analysis(&source, "t105_violation.test.ts");
964 assert!(
965 !fa.has_relational_assertion,
966 "all toBe/toEqual file should not have relational"
967 );
968 }
969
970 #[test]
971 fn relational_assertion_pass_greater_than() {
972 let source = fixture("t105_pass_relational.test.ts");
973 let extractor = TypeScriptExtractor::new();
974 let fa = extractor.extract_file_analysis(&source, "t105_pass_relational.test.ts");
975 assert!(
976 fa.has_relational_assertion,
977 "toBeGreaterThan should set has_relational_assertion"
978 );
979 }
980
981 #[test]
982 fn relational_assertion_pass_truthy() {
983 let source = fixture("t105_pass_truthy.test.ts");
984 let extractor = TypeScriptExtractor::new();
985 let fa = extractor.extract_file_analysis(&source, "t105_pass_truthy.test.ts");
986 assert!(
987 fa.has_relational_assertion,
988 "toBeTruthy should set has_relational_assertion"
989 );
990 }
991
992 #[test]
993 fn query_capture_names_relational_assertion() {
994 let q = make_query(include_str!("../queries/relational_assertion.scm"));
995 assert!(
996 q.capture_index_for_name("relational").is_some(),
997 "relational_assertion.scm must define @relational capture"
998 );
999 }
1000
1001 #[test]
1004 fn wait_and_see_violation_sleep() {
1005 let source = fixture("t108_violation_sleep.test.ts");
1006 let extractor = TypeScriptExtractor::new();
1007 let funcs = extractor.extract_test_functions(&source, "t108_violation_sleep.test.ts");
1008 assert!(!funcs.is_empty());
1009 for func in &funcs {
1010 assert!(
1011 func.analysis.has_wait,
1012 "test '{}' should have has_wait=true",
1013 func.name
1014 );
1015 }
1016 }
1017
1018 #[test]
1019 fn wait_and_see_pass_no_sleep() {
1020 let source = fixture("t108_pass_no_sleep.test.ts");
1021 let extractor = TypeScriptExtractor::new();
1022 let funcs = extractor.extract_test_functions(&source, "t108_pass_no_sleep.test.ts");
1023 assert_eq!(funcs.len(), 1);
1024 assert!(
1025 !funcs[0].analysis.has_wait,
1026 "test without sleep should have has_wait=false"
1027 );
1028 }
1029
1030 #[test]
1031 fn query_capture_names_wait_and_see() {
1032 let q = make_query(include_str!("../queries/wait_and_see.scm"));
1033 assert!(
1034 q.capture_index_for_name("wait").is_some(),
1035 "wait_and_see.scm must define @wait capture"
1036 );
1037 }
1038
1039 #[test]
1042 fn t109_violation_names_detected() {
1043 let source = fixture("t109_violation.test.ts");
1044 let extractor = TypeScriptExtractor::new();
1045 let funcs = extractor.extract_test_functions(&source, "t109_violation.test.ts");
1046 assert!(!funcs.is_empty());
1047 for func in &funcs {
1048 assert!(
1049 exspec_core::rules::is_undescriptive_test_name(&func.name),
1050 "test '{}' should be undescriptive",
1051 func.name
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn t109_pass_descriptive_names() {
1058 let source = fixture("t109_pass.test.ts");
1059 let extractor = TypeScriptExtractor::new();
1060 let funcs = extractor.extract_test_functions(&source, "t109_pass.test.ts");
1061 assert!(!funcs.is_empty());
1062 for func in &funcs {
1063 assert!(
1064 !exspec_core::rules::is_undescriptive_test_name(&func.name),
1065 "test '{}' should be descriptive",
1066 func.name
1067 );
1068 }
1069 }
1070
1071 #[test]
1073 fn t109_cjk_pass_descriptive_names() {
1074 let source = fixture("t109_cjk_pass.test.ts");
1075 let extractor = TypeScriptExtractor::new();
1076 let funcs = extractor.extract_test_functions(&source, "t109_cjk_pass.test.ts");
1077 assert!(!funcs.is_empty());
1078 for func in &funcs {
1079 assert!(
1080 !exspec_core::rules::is_undescriptive_test_name(&func.name),
1081 "CJK test '{}' should be descriptive",
1082 func.name
1083 );
1084 }
1085 }
1086
1087 #[test]
1090 fn t106_violation_duplicate_literal() {
1091 let source = fixture("t106_violation.test.ts");
1092 let extractor = TypeScriptExtractor::new();
1093 let funcs = extractor.extract_test_functions(&source, "t106_violation.test.ts");
1094 assert_eq!(funcs.len(), 1);
1095 assert!(
1096 funcs[0].analysis.duplicate_literal_count >= 3,
1097 "42 appears 3 times, should be >= 3: got {}",
1098 funcs[0].analysis.duplicate_literal_count
1099 );
1100 }
1101
1102 #[test]
1103 fn t106_pass_no_duplicates() {
1104 let source = fixture("t106_pass_no_duplicates.test.ts");
1105 let extractor = TypeScriptExtractor::new();
1106 let funcs = extractor.extract_test_functions(&source, "t106_pass_no_duplicates.test.ts");
1107 assert_eq!(funcs.len(), 1);
1108 assert!(
1109 funcs[0].analysis.duplicate_literal_count < 3,
1110 "each literal appears once: got {}",
1111 funcs[0].analysis.duplicate_literal_count
1112 );
1113 }
1114
1115 #[test]
1118 fn t001_expect_to_throw_already_covered() {
1119 let source = "import { it, expect } from 'vitest';\nit('throws', () => { expect(() => fn()).toThrow(); });";
1121 let extractor = TypeScriptExtractor::new();
1122 let funcs = extractor.extract_test_functions(&source, "test_throw.test.ts");
1123 assert_eq!(funcs.len(), 1);
1124 assert!(
1125 funcs[0].analysis.assertion_count >= 1,
1126 "expect().toThrow() should already be covered, got {}",
1127 funcs[0].analysis.assertion_count
1128 );
1129 }
1130
1131 #[test]
1132 fn t001_rejects_to_throw_counts_as_assertion() {
1133 let source = fixture("t001_rejects_to_throw.test.ts");
1135 let extractor = TypeScriptExtractor::new();
1136 let funcs = extractor.extract_test_functions(&source, "t001_rejects_to_throw.test.ts");
1137 assert_eq!(funcs.len(), 1);
1138 assert!(
1139 funcs[0].analysis.assertion_count >= 1,
1140 "expect().rejects.toThrow() should count as assertion, got {}",
1141 funcs[0].analysis.assertion_count
1142 );
1143 }
1144
1145 #[test]
1146 fn t001_expect_type_of_counts_as_assertion() {
1147 let source = fixture("t001_expect_type_of.test.ts");
1149 let extractor = TypeScriptExtractor::new();
1150 let funcs = extractor.extract_test_functions(&source, "t001_expect_type_of.test.ts");
1151 assert_eq!(funcs.len(), 1);
1152 assert!(
1153 funcs[0].analysis.assertion_count >= 1,
1154 "expectTypeOf() should count as assertion, got {}",
1155 funcs[0].analysis.assertion_count
1156 );
1157 }
1158
1159 #[test]
1162 fn t001_expect_soft_counts_as_assertion() {
1163 let source = fixture("t001_expect_soft.test.ts");
1165 let extractor = TypeScriptExtractor::new();
1166 let funcs = extractor.extract_test_functions(&source, "t001_expect_soft.test.ts");
1167 assert_eq!(funcs.len(), 1);
1168 assert!(
1169 funcs[0].analysis.assertion_count >= 1,
1170 "expect.soft() should count as assertion, got {}",
1171 funcs[0].analysis.assertion_count
1172 );
1173 }
1174
1175 #[test]
1176 fn t001_expect_element_counts_as_assertion() {
1177 let source = fixture("t001_expect_element.test.ts");
1179 let extractor = TypeScriptExtractor::new();
1180 let funcs = extractor.extract_test_functions(&source, "t001_expect_element.test.ts");
1181 assert_eq!(funcs.len(), 1);
1182 assert!(
1183 funcs[0].analysis.assertion_count >= 1,
1184 "expect.element() should count as assertion, got {}",
1185 funcs[0].analysis.assertion_count
1186 );
1187 }
1188
1189 #[test]
1190 fn t001_expect_poll_counts_as_assertion() {
1191 let source = fixture("t001_expect_poll.test.ts");
1193 let extractor = TypeScriptExtractor::new();
1194 let funcs = extractor.extract_test_functions(&source, "t001_expect_poll.test.ts");
1195 assert_eq!(funcs.len(), 1);
1196 assert!(
1197 funcs[0].analysis.assertion_count >= 1,
1198 "expect.poll() should count as assertion, got {}",
1199 funcs[0].analysis.assertion_count
1200 );
1201 }
1202
1203 #[test]
1206 fn t001_chai_property_fixture_all_detected() {
1207 let source = fixture("t001_chai_property.test.ts");
1209 let extractor = TypeScriptExtractor::new();
1210 let funcs = extractor.extract_test_functions(&source, "t001_chai_property.test.ts");
1211 assert_eq!(funcs.len(), 6);
1212 for f in &funcs {
1213 assert!(
1214 f.analysis.assertion_count >= 1,
1215 "test '{}' should have assertion_count >= 1, got {}",
1216 f.name,
1217 f.analysis.assertion_count
1218 );
1219 }
1220 }
1221
1222 #[test]
1223 fn t001_chai_property_depth1_no_double_count() {
1224 let source = r#"
1226import { expect } from 'chai';
1227describe('d', () => {
1228 it('t', () => {
1229 expect(x).ok;
1230 });
1231});
1232"#;
1233 let extractor = TypeScriptExtractor::new();
1234 let funcs = extractor.extract_test_functions(source, "test.ts");
1235 assert_eq!(funcs.len(), 1);
1236 assert_eq!(
1237 funcs[0].analysis.assertion_count, 1,
1238 "depth 1 property should count exactly 1, got {}",
1239 funcs[0].analysis.assertion_count
1240 );
1241 }
1242
1243 #[test]
1244 fn t001_chai_property_depth3_no_double_count() {
1245 let source = r#"
1247import { expect } from 'chai';
1248describe('d', () => {
1249 it('t', () => {
1250 expect(x).to.be.true;
1251 });
1252});
1253"#;
1254 let extractor = TypeScriptExtractor::new();
1255 let funcs = extractor.extract_test_functions(source, "test.ts");
1256 assert_eq!(funcs.len(), 1);
1257 assert_eq!(
1258 funcs[0].analysis.assertion_count, 1,
1259 "depth 3 property should count exactly 1, got {}",
1260 funcs[0].analysis.assertion_count
1261 );
1262 }
1263
1264 #[test]
1265 fn t001_chai_property_depth4_no_double_count() {
1266 let source = r#"
1268import { expect } from 'chai';
1269describe('d', () => {
1270 it('t', () => {
1271 expect(spy).to.have.been.calledOnce;
1272 });
1273});
1274"#;
1275 let extractor = TypeScriptExtractor::new();
1276 let funcs = extractor.extract_test_functions(source, "test.ts");
1277 assert_eq!(funcs.len(), 1);
1278 assert_eq!(
1279 funcs[0].analysis.assertion_count, 1,
1280 "depth 4 property should count exactly 1, got {}",
1281 funcs[0].analysis.assertion_count
1282 );
1283 }
1284
1285 #[test]
1286 fn t001_chai_property_intermediate_not_counted() {
1287 let source = r#"
1290import { expect } from 'chai';
1291describe('d', () => {
1292 it('t', () => {
1293 expect(x).to;
1294 });
1295});
1296"#;
1297 let extractor = TypeScriptExtractor::new();
1298 let funcs = extractor.extract_test_functions(source, "test.ts");
1299 assert_eq!(funcs.len(), 1);
1300 assert_eq!(
1301 funcs[0].analysis.assertion_count, 0,
1302 "intermediate property .to should NOT count as assertion, got {}",
1303 funcs[0].analysis.assertion_count
1304 );
1305 }
1306
1307 #[test]
1310 fn t001_not_modifier_all_detected() {
1311 let source = fixture("t001_not_modifier.test.ts");
1313 let extractor = TypeScriptExtractor::new();
1314 let funcs = extractor.extract_test_functions(&source, "t001_not_modifier.test.ts");
1315 assert_eq!(funcs.len(), 3);
1316 for f in &funcs {
1317 assert_eq!(
1318 f.analysis.assertion_count, 1,
1319 "test '{}' with .not modifier should have assertion_count == 1, got {}",
1320 f.name, f.analysis.assertion_count
1321 );
1322 }
1323 }
1324
1325 #[test]
1326 fn t001_resolves_rejects_chain_all_detected() {
1327 let source = fixture("t001_resolves_rejects_chain.test.ts");
1329 let extractor = TypeScriptExtractor::new();
1330 let funcs =
1331 extractor.extract_test_functions(&source, "t001_resolves_rejects_chain.test.ts");
1332 assert_eq!(funcs.len(), 3);
1333 for f in &funcs {
1334 assert_eq!(
1335 f.analysis.assertion_count, 1,
1336 "test '{}' with modifier chain should have assertion_count == 1, got {}",
1337 f.name, f.analysis.assertion_count
1338 );
1339 }
1340 }
1341
1342 #[test]
1345 fn t001_chai_method_call_fixture_all_detected() {
1346 let source = fixture("t001_chai_method_call.test.ts");
1348 let extractor = TypeScriptExtractor::new();
1349 let funcs = extractor.extract_test_functions(&source, "t001_chai_method_call.test.ts");
1350 assert_eq!(funcs.len(), 18);
1351
1352 assert_eq!(
1354 funcs[0].analysis.assertion_count, 1,
1355 "TC-01 to.equal should count exactly 1, got {}",
1356 funcs[0].analysis.assertion_count
1357 );
1358
1359 assert!(
1361 funcs[1].analysis.assertion_count >= 1,
1362 "TC-02 to.be.a should have assertion_count >= 1, got {}",
1363 funcs[1].analysis.assertion_count
1364 );
1365
1366 assert!(
1368 funcs[2].analysis.assertion_count >= 1,
1369 "TC-03 to.have.callCount should have assertion_count >= 1, got {}",
1370 funcs[2].analysis.assertion_count
1371 );
1372
1373 assert!(
1375 funcs[3].analysis.assertion_count >= 1,
1376 "TC-04 to.have.been.calledWith should have assertion_count >= 1, got {}",
1377 funcs[3].analysis.assertion_count
1378 );
1379
1380 assert!(
1382 funcs[4].analysis.assertion_count >= 1,
1383 "TC-05 to.not.have.been.calledWith should have assertion_count >= 1, got {}",
1384 funcs[4].analysis.assertion_count
1385 );
1386
1387 assert!(
1389 funcs[5].analysis.assertion_count >= 2,
1390 "TC-06 mixed property+method should have assertion_count >= 2, got {}",
1391 funcs[5].analysis.assertion_count
1392 );
1393
1394 assert!(
1396 funcs[6].analysis.assertion_count >= 2,
1397 "TC-07 multiple methods should have assertion_count >= 2, got {}",
1398 funcs[6].analysis.assertion_count
1399 );
1400
1401 assert_eq!(
1403 funcs[7].analysis.assertion_count, 0,
1404 "TC-08 no assertion should have assertion_count == 0, got {}",
1405 funcs[7].analysis.assertion_count
1406 );
1407
1408 assert_eq!(
1410 funcs[8].analysis.assertion_count, 0,
1411 "TC-09 customHelper should have assertion_count == 0, got {}",
1412 funcs[8].analysis.assertion_count
1413 );
1414
1415 assert!(
1417 funcs[9].analysis.assertion_count >= 1,
1418 "TC-10 not.to.equal should have assertion_count >= 1, got {}",
1419 funcs[9].analysis.assertion_count
1420 );
1421
1422 assert_eq!(
1424 funcs[10].analysis.assertion_count, 1,
1425 "TC-11 to.equal regression should count exactly 1, got {}",
1426 funcs[10].analysis.assertion_count
1427 );
1428
1429 assert!(
1431 funcs[11].analysis.assertion_count >= 1,
1432 "TC-12 deep intermediate should have assertion_count >= 1, got {}",
1433 funcs[11].analysis.assertion_count
1434 );
1435
1436 assert!(
1438 funcs[12].analysis.assertion_count >= 1,
1439 "TC-13 nested intermediate should have assertion_count >= 1, got {}",
1440 funcs[12].analysis.assertion_count
1441 );
1442
1443 assert!(
1445 funcs[13].analysis.assertion_count >= 1,
1446 "TC-14 own intermediate should have assertion_count >= 1, got {}",
1447 funcs[13].analysis.assertion_count
1448 );
1449
1450 assert!(
1452 funcs[14].analysis.assertion_count >= 1,
1453 "TC-15 ordered intermediate should have assertion_count >= 1, got {}",
1454 funcs[14].analysis.assertion_count
1455 );
1456
1457 assert!(
1459 funcs[15].analysis.assertion_count >= 1,
1460 "TC-16 any intermediate should have assertion_count >= 1, got {}",
1461 funcs[15].analysis.assertion_count
1462 );
1463
1464 assert!(
1466 funcs[16].analysis.assertion_count >= 1,
1467 "TC-17 all intermediate should have assertion_count >= 1, got {}",
1468 funcs[16].analysis.assertion_count
1469 );
1470
1471 assert!(
1473 funcs[17].analysis.assertion_count >= 1,
1474 "TC-18 itself intermediate should have assertion_count >= 1, got {}",
1475 funcs[17].analysis.assertion_count
1476 );
1477 }
1478
1479 #[test]
1480 fn t001_chai_method_call_depth2_no_double_count() {
1481 let source = r#"
1483import { expect } from 'chai';
1484describe('d', () => {
1485 it('t', () => {
1486 expect(x).to.equal(y);
1487 });
1488});
1489"#;
1490 let extractor = TypeScriptExtractor::new();
1491 let funcs = extractor.extract_test_functions(source, "test.ts");
1492 assert_eq!(funcs.len(), 1);
1493 assert_eq!(
1494 funcs[0].analysis.assertion_count, 1,
1495 "depth 2 method-call should count exactly 1, got {}",
1496 funcs[0].analysis.assertion_count
1497 );
1498 }
1499
1500 #[test]
1501 fn t001_chai_deep_intermediate_no_double_count() {
1502 let source = r#"
1504import { expect } from 'chai';
1505describe('d', () => {
1506 it('t', () => {
1507 expect(obj).to.have.deep.equal({a: 1});
1508 });
1509});
1510"#;
1511 let extractor = TypeScriptExtractor::new();
1512 let funcs = extractor.extract_test_functions(source, "test.ts");
1513 assert_eq!(funcs.len(), 1);
1514 assert_eq!(
1515 funcs[0].analysis.assertion_count, 1,
1516 "deep intermediate should count exactly 1, got {}",
1517 funcs[0].analysis.assertion_count
1518 );
1519 }
1520
1521 #[test]
1522 fn t001_expect_soft_chain_fixture() {
1523 let source = fixture("t001_expect_soft_chain.test.ts");
1525 let extractor = TypeScriptExtractor::new();
1526 let funcs = extractor.extract_test_functions(&source, "t001_expect_soft_chain.test.ts");
1527 assert_eq!(funcs.len(), 10);
1528
1529 assert!(
1531 funcs[0].analysis.assertion_count >= 1,
1532 "B1 expect.soft depth-2 should have assertion_count >= 1, got {}",
1533 funcs[0].analysis.assertion_count
1534 );
1535
1536 assert!(
1538 funcs[1].analysis.assertion_count >= 1,
1539 "B2 expect.soft.not depth-3 should have assertion_count >= 1, got {}",
1540 funcs[1].analysis.assertion_count
1541 );
1542
1543 assert!(
1545 funcs[2].analysis.assertion_count >= 1,
1546 "B3 expect.soft.resolves depth-3 should have assertion_count >= 1, got {}",
1547 funcs[2].analysis.assertion_count
1548 );
1549
1550 assert!(
1552 funcs[3].analysis.assertion_count >= 1,
1553 "B4 expect.soft.rejects depth-3 should have assertion_count >= 1, got {}",
1554 funcs[3].analysis.assertion_count
1555 );
1556
1557 assert!(
1559 funcs[4].analysis.assertion_count >= 1,
1560 "B5 expect.soft.resolves.not depth-4 should have assertion_count >= 1, got {}",
1561 funcs[4].analysis.assertion_count
1562 );
1563
1564 assert!(
1566 funcs[5].analysis.assertion_count >= 1,
1567 "B6 expect.soft.rejects.not depth-4 should have assertion_count >= 1, got {}",
1568 funcs[5].analysis.assertion_count
1569 );
1570
1571 assert_eq!(
1573 funcs[6].analysis.assertion_count, 0,
1574 "B7 customHelper should have assertion_count == 0, got {}",
1575 funcs[6].analysis.assertion_count
1576 );
1577
1578 assert_eq!(
1580 funcs[7].analysis.assertion_count, 0,
1581 "B8 no assertion should have assertion_count == 0, got {}",
1582 funcs[7].analysis.assertion_count
1583 );
1584
1585 assert!(
1587 funcs[8].analysis.assertion_count >= 1,
1588 "B9 expect.element.not depth-3 should have assertion_count >= 1, got {}",
1589 funcs[8].analysis.assertion_count
1590 );
1591
1592 assert!(
1594 funcs[9].analysis.assertion_count >= 1,
1595 "B10 expect.poll.not depth-3 should have assertion_count >= 1, got {}",
1596 funcs[9].analysis.assertion_count
1597 );
1598 }
1599
1600 #[test]
1603 fn t001_supertest_expect_method_call() {
1604 let source = fixture("t001_supertest.test.ts");
1605 let extractor = TypeScriptExtractor::new();
1606 let funcs = extractor.extract_test_functions(&source, "t001_supertest.test.ts");
1607 assert_eq!(funcs.len(), 6);
1608
1609 assert_eq!(
1611 funcs[0].analysis.assertion_count, 1,
1612 "TC-01 single .expect(200) should have assertion_count == 1, got {}",
1613 funcs[0].analysis.assertion_count
1614 );
1615
1616 assert_eq!(
1618 funcs[1].analysis.assertion_count, 2,
1619 "TC-02 two .expect() should have assertion_count == 2, got {}",
1620 funcs[1].analysis.assertion_count
1621 );
1622
1623 assert_eq!(
1625 funcs[2].analysis.assertion_count, 2,
1626 "TC-03 .set() + two .expect() should have assertion_count == 2, got {}",
1627 funcs[2].analysis.assertion_count
1628 );
1629
1630 assert_eq!(
1632 funcs[3].analysis.assertion_count, 0,
1633 "TC-04 no assertion should have assertion_count == 0, got {}",
1634 funcs[3].analysis.assertion_count
1635 );
1636
1637 assert_eq!(
1639 funcs[4].analysis.assertion_count, 1,
1640 "TC-05 standalone expect should have assertion_count == 1, got {}",
1641 funcs[4].analysis.assertion_count
1642 );
1643
1644 assert_eq!(
1646 funcs[5].analysis.assertion_count, 1,
1647 "TC-06 non-supertest builder .expect() should have assertion_count == 1, got {}",
1648 funcs[5].analysis.assertion_count
1649 );
1650 }
1651
1652 #[test]
1655 fn t001_chai_vocab_expansion_fixture_all_detected() {
1656 let source = fixture("t001_chai_vocab_expansion.test.ts");
1657 let extractor = TypeScriptExtractor::new();
1658 let funcs = extractor.extract_test_functions(&source, "t001_chai_vocab_expansion.test.ts");
1659 assert_eq!(funcs.len(), 18);
1660
1661 for (i, f) in funcs.iter().enumerate().take(16) {
1663 assert!(
1664 f.analysis.assertion_count >= 1,
1665 "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1666 i + 1,
1667 f.name,
1668 f.analysis.assertion_count
1669 );
1670 }
1671
1672 assert_eq!(
1674 funcs[16].analysis.assertion_count, 0,
1675 "TC-17 sinon.stub should have assertion_count == 0, got {}",
1676 funcs[16].analysis.assertion_count
1677 );
1678
1679 assert_eq!(
1681 funcs[17].analysis.assertion_count, 0,
1682 "TC-18 no assertion should have assertion_count == 0, got {}",
1683 funcs[17].analysis.assertion_count
1684 );
1685 }
1686
1687 #[test]
1690 fn t001_chai_property_arrow_fixture_all_detected() {
1691 let source = fixture("t001_chai_property_arrow.test.ts");
1692 let extractor = TypeScriptExtractor::new();
1693 let funcs = extractor.extract_test_functions(&source, "t001_chai_property_arrow.test.ts");
1694 assert_eq!(funcs.len(), 8);
1695
1696 for (i, f) in funcs.iter().enumerate().take(7) {
1698 assert!(
1699 f.analysis.assertion_count >= 1,
1700 "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1701 i + 1,
1702 f.name,
1703 f.analysis.assertion_count
1704 );
1705 }
1706
1707 assert_eq!(
1709 funcs[7].analysis.assertion_count, 0,
1710 "TC-08 no assertion should have assertion_count == 0, got {}",
1711 funcs[7].analysis.assertion_count
1712 );
1713 }
1714
1715 #[test]
1716 fn t107_skipped_for_typescript() {
1717 let source = fixture("t107_pass.test.ts");
1720 let extractor = TypeScriptExtractor::new();
1721 let funcs = extractor.extract_test_functions(&source, "t107_pass.test.ts");
1722 assert_eq!(funcs.len(), 1);
1723 let analysis = &funcs[0].analysis;
1724 assert!(
1725 analysis.assertion_count >= 2,
1726 "fixture should have 2+ assertions: got {}",
1727 analysis.assertion_count
1728 );
1729 assert_eq!(
1730 analysis.assertion_message_count, analysis.assertion_count,
1731 "TS assertion_message_count should equal assertion_count to skip T107"
1732 );
1733 }
1734
1735 #[test]
1737 fn t001_custom_helper_with_config_no_violation() {
1738 use exspec_core::query_utils::apply_custom_assertion_fallback;
1739 use exspec_core::rules::{evaluate_rules, Config};
1740
1741 let source = fixture("t001_custom_helper.test.ts");
1742 let extractor = TypeScriptExtractor::new();
1743 let mut analysis = extractor.extract_file_analysis(&source, "t001_custom_helper.test.ts");
1744 let patterns = vec!["myAssert(".to_string()];
1745 apply_custom_assertion_fallback(&mut analysis, &source, &patterns);
1746
1747 let config = Config::default();
1748 let diags = evaluate_rules(&analysis.functions, &config);
1749 let t001_diags: Vec<_> = diags.iter().filter(|d| d.rule.0 == "T001").collect();
1750 assert_eq!(
1754 t001_diags.len(),
1755 1,
1756 "only 'has no assertion' test should trigger T001"
1757 );
1758 }
1759
1760 #[test]
1763 fn t001_expect_assertions_counts_as_assertion() {
1764 let source = fixture("t001_expect_assertions.test.ts");
1766 let extractor = TypeScriptExtractor::new();
1767 let funcs = extractor.extract_test_functions(&source, "t001_expect_assertions.test.ts");
1768 assert_eq!(funcs.len(), 8);
1770
1771 assert!(
1773 funcs[0].analysis.assertion_count >= 1,
1774 "expect.assertions(N) should count as assertion, got {}",
1775 funcs[0].analysis.assertion_count
1776 );
1777
1778 assert_eq!(
1780 funcs[1].analysis.assertion_count, 1,
1781 "expect.assertions(0) should count as exactly 1 assertion, got {}",
1782 funcs[1].analysis.assertion_count
1783 );
1784
1785 assert!(
1787 funcs[2].analysis.assertion_count >= 1,
1788 "expect.hasAssertions() should count as assertion, got {}",
1789 funcs[2].analysis.assertion_count
1790 );
1791
1792 assert_eq!(
1794 funcs[3].analysis.assertion_count, 1,
1795 "expect.unreachable() should count as exactly 1 assertion, got {}",
1796 funcs[3].analysis.assertion_count
1797 );
1798
1799 assert_eq!(
1801 funcs[4].analysis.assertion_count, 1,
1802 "expectType<T>(value) should count as exactly 1 assertion, got {}",
1803 funcs[4].analysis.assertion_count
1804 );
1805
1806 assert!(
1808 funcs[5].analysis.assertion_count >= 2,
1809 "mixed expect.assertions + expect().toBe() should count 2+, got {}",
1810 funcs[5].analysis.assertion_count
1811 );
1812
1813 assert!(
1815 funcs[6].analysis.assertion_count >= 2,
1816 "expectType + expectTypeOf should count 2+, got {}",
1817 funcs[6].analysis.assertion_count
1818 );
1819
1820 assert_eq!(
1822 funcs[7].analysis.assertion_count, 0,
1823 "no-assertion test should have assertion_count == 0"
1824 );
1825 }
1826
1827 #[test]
1830 fn t001_chai_nestjs_aliases_fixture() {
1831 let source = fixture("t001_chai_nestjs_aliases.test.ts");
1832 let extractor = TypeScriptExtractor::new();
1833 let funcs = extractor.extract_test_functions(&source, "t001_chai_nestjs_aliases.test.ts");
1834 assert_eq!(funcs.len(), 9);
1835
1836 assert!(
1838 funcs[0].analysis.assertion_count >= 1,
1839 "TC-01 instanceof alias should have assertion_count >= 1, got {}",
1840 funcs[0].analysis.assertion_count
1841 );
1842
1843 assert!(
1845 funcs[1].analysis.assertion_count >= 1,
1846 "TC-02 throws alias should have assertion_count >= 1, got {}",
1847 funcs[1].analysis.assertion_count
1848 );
1849
1850 assert!(
1852 funcs[2].analysis.assertion_count >= 1,
1853 "TC-03 contains alias should have assertion_count >= 1, got {}",
1854 funcs[2].analysis.assertion_count
1855 );
1856
1857 assert!(
1859 funcs[3].analysis.assertion_count >= 1,
1860 "TC-04 equals alias should have assertion_count >= 1, got {}",
1861 funcs[3].analysis.assertion_count
1862 );
1863
1864 assert!(
1866 funcs[4].analysis.assertion_count >= 1,
1867 "TC-05 ownProperty should have assertion_count >= 1, got {}",
1868 funcs[4].analysis.assertion_count
1869 );
1870
1871 assert!(
1873 funcs[5].analysis.assertion_count >= 1,
1874 "TC-06 length alias should have assertion_count >= 1, got {}",
1875 funcs[5].analysis.assertion_count
1876 );
1877
1878 assert!(
1880 funcs[6].analysis.assertion_count >= 1,
1881 "TC-07 throw property should have assertion_count >= 1, got {}",
1882 funcs[6].analysis.assertion_count
1883 );
1884
1885 assert!(
1887 funcs[7].analysis.assertion_count >= 1,
1888 "TC-08 and+instanceof deep chain should have assertion_count >= 1, got {}",
1889 funcs[7].analysis.assertion_count
1890 );
1891
1892 assert_eq!(
1894 funcs[8].analysis.assertion_count, 0,
1895 "TC-09 no assertion should have assertion_count == 0, got {}",
1896 funcs[8].analysis.assertion_count
1897 );
1898 }
1899
1900 #[test]
1903 fn t001_sinon_verify_fixture_all_detected() {
1904 let source = fixture("t001_sinon_verify.test.ts");
1905 let extractor = TypeScriptExtractor::new();
1906 let funcs = extractor.extract_test_functions(&source, "t001_sinon_verify.test.ts");
1907 assert_eq!(funcs.len(), 7);
1908
1909 for (i, f) in funcs.iter().enumerate().take(5) {
1911 assert!(
1912 f.analysis.assertion_count >= 1,
1913 "TC-{:02} '{}' should have assertion_count >= 1, got {}",
1914 i + 1,
1915 f.name,
1916 f.analysis.assertion_count
1917 );
1918 }
1919
1920 assert!(
1922 funcs[4].analysis.assertion_count >= 2,
1923 "TC-05 verify + expect should have assertion_count >= 2, got {}",
1924 funcs[4].analysis.assertion_count
1925 );
1926
1927 assert_eq!(
1929 funcs[5].analysis.assertion_count, 0,
1930 "TC-06 mock.restore should have assertion_count == 0, got {}",
1931 funcs[5].analysis.assertion_count
1932 );
1933
1934 assert_eq!(
1936 funcs[6].analysis.assertion_count, 0,
1937 "TC-07 no assertion should have assertion_count == 0, got {}",
1938 funcs[6].analysis.assertion_count
1939 );
1940 }
1941}