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