use anyhow::{Context, Result};
use regex::Regex;
use semver_analyzer_core::{TestConvention, TestDiff, TestFile};
use std::path::Path;
use std::process::Command;
use std::sync::LazyLock;
#[derive(Default)]
pub struct TsTestAnalyzer;
impl TsTestAnalyzer {
pub fn new() -> Self {
Self
}
}
impl TsTestAnalyzer {
pub fn find_tests(&self, repo: &Path, source_file: &Path) -> Result<Vec<TestFile>> {
find_test_files(repo, source_file)
}
pub fn diff_test_assertions(
&self,
repo: &Path,
test_file: &TestFile,
from_ref: &str,
to_ref: &str,
) -> Result<TestDiff> {
diff_test_file(repo, test_file, from_ref, to_ref)
}
}
const SOURCE_EXTENSIONS: &[&str] = &[".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"];
const TEST_EXTENSIONS: &[&str] = &[
".test.ts",
".test.tsx",
".spec.ts",
".spec.tsx",
".test.js",
".test.jsx",
".spec.js",
".spec.jsx",
];
fn find_test_files(repo: &Path, source_file: &Path) -> Result<Vec<TestFile>> {
let mut found = Vec::new();
let mut seen = std::collections::HashSet::new();
let file_name = source_file
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let stem = strip_source_extension(file_name);
let parent = source_file.parent().unwrap_or(Path::new(""));
let mut add = |path: std::path::PathBuf, convention: TestConvention| {
if seen.insert(path.clone()) {
found.push(TestFile { path, convention });
}
};
for ext in TEST_EXTENSIONS {
let test_path = parent.join(format!("{}{}", stem, ext));
let full_path = repo.join(&test_path);
if full_path.exists() {
let convention = if ext.contains(".test.") {
TestConvention::DotTest
} else {
TestConvention::DotSpec
};
add(test_path, convention);
}
}
let tests_dir = parent.join("__tests__");
if repo.join(&tests_dir).is_dir() {
for base_ext in &[".ts", ".tsx"] {
let plain = tests_dir.join(format!("{}{}", stem, base_ext));
if repo.join(&plain).exists() {
add(plain, TestConvention::TestsDir);
}
}
for ext in TEST_EXTENSIONS {
let test_path = tests_dir.join(format!("{}{}", stem, ext));
if repo.join(&test_path).exists() {
add(test_path, TestConvention::TestsDir);
}
}
if let Ok(entries) = std::fs::read_dir(repo.join(&tests_dir)) {
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_dir() {
let subdir_name = entry.file_name().to_string_lossy().to_string();
if subdir_name == "__snapshots__" {
continue;
}
for ext in TEST_EXTENSIONS {
let test_path = tests_dir
.join(&subdir_name)
.join(format!("{}{}", stem, ext));
if repo.join(&test_path).exists() {
add(test_path, TestConvention::TestsDir);
}
}
}
}
}
collect_test_files_in_dir(repo, &tests_dir, &mut |path| {
add(path, TestConvention::TestsDir);
});
}
if let Some(grandparent) = parent.parent() {
let dir_name = parent.file_name().and_then(|n| n.to_str()).unwrap_or("");
let parent_tests_dir = grandparent.join("__tests__").join(dir_name);
if repo.join(&parent_tests_dir).is_dir() {
for ext in TEST_EXTENSIONS {
let test_path = parent_tests_dir.join(format!("{}{}", stem, ext));
if repo.join(&test_path).exists() {
add(test_path, TestConvention::TestsDir);
}
}
}
}
if let Some(parent_name) = infer_parent_component_name(stem) {
for ext in TEST_EXTENSIONS {
let test_path = parent.join(format!("{}{}", parent_name, ext));
if repo.join(&test_path).exists() {
let convention = if ext.contains(".test.") {
TestConvention::DotTest
} else {
TestConvention::DotSpec
};
add(test_path, convention);
}
let tests_dir = parent.join("__tests__");
let test_path = tests_dir.join(format!("{}{}", parent_name, ext));
if repo.join(&test_path).exists() {
add(test_path, TestConvention::TestsDir);
}
}
}
Ok(found)
}
fn collect_test_files_in_dir(repo: &Path, dir: &Path, add: &mut dyn FnMut(std::path::PathBuf)) {
let full_dir = repo.join(dir);
let entries = match std::fs::read_dir(&full_dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let entry_path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if entry_path.is_dir() {
if name == "__snapshots__" {
continue;
}
collect_test_files_in_dir(repo, &dir.join(&name), add);
} else if is_test_file(&name) {
add(dir.join(&name));
}
}
}
fn is_test_file(filename: &str) -> bool {
TEST_EXTENSIONS.iter().any(|ext| filename.ends_with(ext))
|| (filename.ends_with(".ts") || filename.ends_with(".tsx")) && !filename.ends_with(".d.ts")
}
fn infer_parent_component_name(stem: &str) -> Option<String> {
const SUFFIXES: &[&str] = &[
"Step",
"Item",
"Header",
"Footer",
"Body",
"Content",
"Title",
"Icon",
"Action",
"Toggle",
"Button",
"Close",
"Group",
"List",
"Container",
"Section",
"Panel",
"Brand",
"Nav",
"Box",
"CloseButton",
"BoxTitle",
"BoxCloseButton",
"HeaderIcon",
"ExpandableContent",
"ToggleGroup",
];
let mut suffixes: Vec<&&str> = SUFFIXES.iter().collect();
suffixes.sort_by_key(|b| std::cmp::Reverse(b.len()));
for suffix in suffixes {
if let Some(prefix) = stem.strip_suffix(suffix) {
if prefix.len() >= 2 && prefix.chars().next().is_some_and(|c| c.is_uppercase()) {
return Some(prefix.to_string());
}
}
}
None
}
fn strip_source_extension(filename: &str) -> &str {
for ext in SOURCE_EXTENSIONS {
if let Some(stem) = filename.strip_suffix(ext) {
return stem;
}
}
filename
.rfind('.')
.map(|i| &filename[..i])
.unwrap_or(filename)
}
static ASSERTION_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
vec![
Regex::new(r"expect\s*\(").unwrap(),
Regex::new(r"\.toBe\s*\(").unwrap(),
Regex::new(r"\.toEqual\s*\(").unwrap(),
Regex::new(r"\.toStrictEqual\s*\(").unwrap(),
Regex::new(r"\.toContain\s*\(").unwrap(),
Regex::new(r"\.toMatch\s*\(").unwrap(),
Regex::new(r"\.toThrow\s*\(").unwrap(),
Regex::new(r"\.toThrowError\s*\(").unwrap(),
Regex::new(r"\.toHaveBeenCalled").unwrap(),
Regex::new(r"\.toHaveBeenCalledWith\s*\(").unwrap(),
Regex::new(r"\.toHaveBeenCalledTimes\s*\(").unwrap(),
Regex::new(r"\.toHaveLength\s*\(").unwrap(),
Regex::new(r"\.toHaveProperty\s*\(").unwrap(),
Regex::new(r"\.toBeNull\b").unwrap(),
Regex::new(r"\.toBeUndefined\b").unwrap(),
Regex::new(r"\.toBeDefined\b").unwrap(),
Regex::new(r"\.toBeTruthy\b").unwrap(),
Regex::new(r"\.toBeFalsy\b").unwrap(),
Regex::new(r"\.toBeGreaterThan\s*\(").unwrap(),
Regex::new(r"\.toBeLessThan\s*\(").unwrap(),
Regex::new(r"\.toBeInstanceOf\s*\(").unwrap(),
Regex::new(r"\.toHaveClass\s*\(").unwrap(),
Regex::new(r"\.toHaveTextContent\s*\(").unwrap(),
Regex::new(r"\.toHaveAttribute\s*\(").unwrap(),
Regex::new(r"\.toBeInTheDocument\b").unwrap(),
Regex::new(r"\.toBeVisible\b").unwrap(),
Regex::new(r"\.resolves\.").unwrap(),
Regex::new(r"\.rejects\.").unwrap(),
Regex::new(r"\.not\.").unwrap(),
Regex::new(r"assert\s*\(").unwrap(),
Regex::new(r"assert\.\w+\s*\(").unwrap(),
Regex::new(r"\.should\.").unwrap(),
Regex::new(r"\.to\.be\.").unwrap(),
Regex::new(r"\.to\.equal\s*\(").unwrap(),
Regex::new(r"\.to\.have\.").unwrap(),
Regex::new(r"\.to\.include\s*\(").unwrap(),
Regex::new(r"\.to\.throw\s*\(").unwrap(),
Regex::new(r"\.to\.deep\.equal\s*\(").unwrap(),
]
});
fn is_assertion_line(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with('*') {
return false;
}
ASSERTION_PATTERNS.iter().any(|pat| pat.is_match(trimmed))
}
fn diff_test_file(
repo: &Path,
test_file: &TestFile,
from_ref: &str,
to_ref: &str,
) -> Result<TestDiff> {
let full_diff = git_diff_file(repo, from_ref, to_ref, &test_file.path)?;
let mut removed_assertions = Vec::new();
let mut added_assertions = Vec::new();
for line in full_diff.lines() {
if line.starts_with('-') && !line.starts_with("---") {
let content = &line[1..]; if is_assertion_line(content) {
removed_assertions.push(content.trim().to_string());
}
} else if line.starts_with('+') && !line.starts_with("+++") {
let content = &line[1..]; if is_assertion_line(content) {
added_assertions.push(content.trim().to_string());
}
}
}
let has_assertion_changes = !removed_assertions.is_empty() || !added_assertions.is_empty();
Ok(TestDiff {
test_file: test_file.path.clone(),
removed_assertions,
added_assertions,
has_assertion_changes,
full_diff,
})
}
fn git_diff_file(repo: &Path, from_ref: &str, to_ref: &str, file_path: &Path) -> Result<String> {
let output = Command::new("git")
.args([
"diff",
&format!("{}..{}", from_ref, to_ref),
"--",
&file_path.to_string_lossy(),
])
.current_dir(repo)
.output()
.with_context(|| format!("Failed to run git diff for {}", file_path.display()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git diff failed for {}: {}", file_path.display(), stderr);
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn strip_ts_extension() {
assert_eq!(strip_source_extension("users.ts"), "users");
assert_eq!(strip_source_extension("Button.tsx"), "Button");
assert_eq!(strip_source_extension("utils.js"), "utils");
assert_eq!(strip_source_extension("app.jsx"), "app");
assert_eq!(strip_source_extension("lib.mts"), "lib");
}
#[test]
fn strip_unknown_extension() {
assert_eq!(strip_source_extension("data.json"), "data");
assert_eq!(strip_source_extension("noext"), "noext");
}
#[test]
fn detects_jest_expect() {
assert!(is_assertion_line(" expect(result).toBe(5);"));
assert!(is_assertion_line("expect(fn).toThrow();"));
assert!(is_assertion_line(" expect(list).toHaveLength(3);"));
assert!(is_assertion_line("expect(obj).toEqual({ a: 1 });"));
assert!(is_assertion_line("expect(cb).toHaveBeenCalledWith('foo');"));
assert!(is_assertion_line("expect(cb).toHaveBeenCalledTimes(2);"));
}
#[test]
fn detects_jest_negated() {
assert!(is_assertion_line("expect(result).not.toBe(null);"));
assert!(is_assertion_line("expect(el).not.toBeInTheDocument();"));
}
#[test]
fn detects_testing_library() {
assert!(is_assertion_line("expect(el).toBeInTheDocument();"));
assert!(is_assertion_line("expect(el).toHaveTextContent('hello');"));
assert!(is_assertion_line("expect(el).toHaveClass('active');"));
assert!(is_assertion_line("expect(el).toBeVisible();"));
assert!(is_assertion_line(
"expect(el).toHaveAttribute('role', 'button');"
));
}
#[test]
fn detects_chai_assertions() {
assert!(is_assertion_line("result.should.equal(5);"));
assert!(is_assertion_line("expect(x).to.be.true;"));
assert!(is_assertion_line("expect(x).to.equal(5);"));
assert!(is_assertion_line("expect(x).to.have.property('name');"));
assert!(is_assertion_line("expect(x).to.include('foo');"));
assert!(is_assertion_line("expect(fn).to.throw(Error);"));
assert!(is_assertion_line("expect(x).to.deep.equal({ a: 1 });"));
}
#[test]
fn detects_node_assert() {
assert!(is_assertion_line("assert(result === true);"));
assert!(is_assertion_line("assert.equal(a, b);"));
assert!(is_assertion_line("assert.strictEqual(a, b);"));
assert!(is_assertion_line("assert.deepEqual(a, b);"));
}
#[test]
fn detects_async_assertions() {
assert!(is_assertion_line("await expect(promise).resolves.toBe(5);"));
assert!(is_assertion_line(
"await expect(promise).rejects.toThrow();"
));
}
#[test]
fn rejects_non_assertions() {
assert!(!is_assertion_line("const result = calculate();"));
assert!(!is_assertion_line("// expect(result).toBe(5);"));
assert!(!is_assertion_line(" * expect(result).toBe(5);"));
assert!(!is_assertion_line(""));
assert!(!is_assertion_line(" "));
assert!(!is_assertion_line("import { expect } from 'vitest';"));
assert!(!is_assertion_line("describe('test', () => {"));
assert!(!is_assertion_line("it('should work', () => {"));
}
#[test]
fn parse_diff_finds_changed_assertions() {
let diff = r#"diff --git a/test.ts b/test.ts
index abc..def 100644
--- a/test.ts
+++ b/test.ts
@@ -10,3 +10,3 @@
- expect(result).toBe(5);
+ expect(result).toBe(10);
const x = 1;
- expect(list).toHaveLength(3);
+ expect(list).toHaveLength(5);
"#;
let mut removed = Vec::new();
let mut added = Vec::new();
for line in diff.lines() {
if line.starts_with('-') && !line.starts_with("---") {
let content = &line[1..];
if is_assertion_line(content) {
removed.push(content.trim().to_string());
}
} else if line.starts_with('+') && !line.starts_with("+++") {
let content = &line[1..];
if is_assertion_line(content) {
added.push(content.trim().to_string());
}
}
}
assert_eq!(removed.len(), 2);
assert_eq!(added.len(), 2);
assert!(removed[0].contains("toBe(5)"));
assert!(added[0].contains("toBe(10)"));
assert!(removed[1].contains("toHaveLength(3)"));
assert!(added[1].contains("toHaveLength(5)"));
}
#[test]
fn parse_diff_ignores_non_assertion_changes() {
let diff = r#"diff --git a/test.ts b/test.ts
--- a/test.ts
+++ b/test.ts
@@ -5,3 +5,3 @@
- const name = 'Alice';
+ const name = 'Bob';
- // This is a comment
+ // Updated comment
"#;
let mut removed = Vec::new();
let mut added = Vec::new();
for line in diff.lines() {
if line.starts_with('-') && !line.starts_with("---") {
let content = &line[1..];
if is_assertion_line(content) {
removed.push(content.trim().to_string());
}
} else if line.starts_with('+') && !line.starts_with("+++") {
let content = &line[1..];
if is_assertion_line(content) {
added.push(content.trim().to_string());
}
}
}
assert!(removed.is_empty());
assert!(added.is_empty());
}
#[test]
fn parse_diff_new_assertion_added() {
let diff = r#"diff --git a/test.ts b/test.ts
--- a/test.ts
+++ b/test.ts
@@ -10,2 +10,4 @@
expect(result).toBe(5);
+ expect(result).toBeGreaterThan(0);
+ expect(result).toBeLessThan(100);
"#;
let mut added = Vec::new();
for line in diff.lines() {
if line.starts_with('+') && !line.starts_with("+++") {
let content = &line[1..];
if is_assertion_line(content) {
added.push(content.trim().to_string());
}
}
}
assert_eq!(added.len(), 2);
assert!(added[0].contains("toBeGreaterThan"));
assert!(added[1].contains("toBeLessThan"));
}
#[test]
fn parse_diff_assertion_removed() {
let diff = r#"diff --git a/test.ts b/test.ts
--- a/test.ts
+++ b/test.ts
@@ -10,3 +10,1 @@
- expect(result).toThrow();
- expect(result).toThrowError('invalid');
const cleanup = true;
"#;
let mut removed = Vec::new();
for line in diff.lines() {
if line.starts_with('-') && !line.starts_with("---") {
let content = &line[1..];
if is_assertion_line(content) {
removed.push(content.trim().to_string());
}
}
}
assert_eq!(removed.len(), 2);
assert!(removed[0].contains("toThrow()"));
assert!(removed[1].contains("toThrowError"));
}
#[test]
fn find_tests_sibling_test() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("users.ts"), "export function foo() {}").unwrap();
std::fs::write(src.join("users.test.ts"), "test('foo', () => {});").unwrap();
let found = find_test_files(dir.path(), Path::new("src/users.ts")).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].path, PathBuf::from("src/users.test.ts"));
assert_eq!(found[0].convention, TestConvention::DotTest);
}
#[test]
fn find_tests_sibling_spec() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("users.ts"), "").unwrap();
std::fs::write(src.join("users.spec.ts"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/users.ts")).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].convention, TestConvention::DotSpec);
}
#[test]
fn find_tests_tsx() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("Button.tsx"), "").unwrap();
std::fs::write(src.join("Button.test.tsx"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/Button.tsx")).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].path, PathBuf::from("src/Button.test.tsx"));
}
#[test]
fn find_tests_in_tests_dir() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
let tests = src.join("__tests__");
std::fs::create_dir_all(&tests).unwrap();
std::fs::write(src.join("users.ts"), "").unwrap();
std::fs::write(tests.join("users.ts"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/users.ts")).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].path, PathBuf::from("src/__tests__/users.ts"));
assert_eq!(found[0].convention, TestConvention::TestsDir);
}
#[test]
fn find_tests_in_tests_dir_with_test_suffix() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
let tests = src.join("__tests__");
std::fs::create_dir_all(&tests).unwrap();
std::fs::write(src.join("users.ts"), "").unwrap();
std::fs::write(tests.join("users.test.ts"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/users.ts")).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].path, PathBuf::from("src/__tests__/users.test.ts"));
}
#[test]
fn find_tests_multiple_matches() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
let tests = src.join("__tests__");
std::fs::create_dir_all(&tests).unwrap();
std::fs::write(src.join("users.ts"), "").unwrap();
std::fs::write(src.join("users.test.ts"), "").unwrap();
std::fs::write(src.join("users.spec.ts"), "").unwrap();
std::fs::write(tests.join("users.test.ts"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/users.ts")).unwrap();
assert_eq!(found.len(), 3);
}
#[test]
fn find_tests_no_match() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("users.ts"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/users.ts")).unwrap();
assert!(found.is_empty());
}
#[test]
fn find_tests_in_nested_tests_subdir() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("Popover");
let tests = src.join("__tests__").join("Generated");
std::fs::create_dir_all(&tests).unwrap();
std::fs::write(src.join("PopoverHeader.tsx"), "").unwrap();
std::fs::write(tests.join("PopoverHeader.test.tsx"), "").unwrap();
let found =
find_test_files(dir.path(), Path::new("src/Popover/PopoverHeader.tsx")).unwrap();
assert!(
found
.iter()
.any(|f| f.path
== Path::new("src/Popover/__tests__/Generated/PopoverHeader.test.tsx")),
"Should find test in __tests__/Generated/ subdir, got: {:?}",
found.iter().map(|f| &f.path).collect::<Vec<_>>()
);
}
#[test]
fn find_tests_parent_component_name() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("Slider");
let tests = src.join("__tests__");
std::fs::create_dir_all(&tests).unwrap();
std::fs::write(src.join("SliderStep.tsx"), "").unwrap();
std::fs::write(tests.join("Slider.test.tsx"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/Slider/SliderStep.tsx")).unwrap();
assert!(
found
.iter()
.any(|f| f.path == Path::new("src/Slider/__tests__/Slider.test.tsx")),
"Should find parent component test, got: {:?}",
found.iter().map(|f| &f.path).collect::<Vec<_>>()
);
}
#[test]
fn find_tests_parent_component_sibling() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("Card");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("CardHeader.tsx"), "").unwrap();
std::fs::write(src.join("Card.test.tsx"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/Card/CardHeader.tsx")).unwrap();
assert!(
found
.iter()
.any(|f| f.path == Path::new("src/Card/Card.test.tsx")),
"Should find parent component sibling test, got: {:?}",
found.iter().map(|f| &f.path).collect::<Vec<_>>()
);
}
#[test]
fn find_tests_directory_level_all_tests() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("Nav");
let tests = src.join("__tests__");
std::fs::create_dir_all(&tests).unwrap();
std::fs::write(src.join("NavItem.tsx"), "").unwrap();
std::fs::write(tests.join("Nav.test.tsx"), "").unwrap();
std::fs::write(tests.join("NavExpandable.test.tsx"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/Nav/NavItem.tsx")).unwrap();
let paths: Vec<_> = found
.iter()
.map(|f| f.path.to_string_lossy().to_string())
.collect();
assert!(
paths.iter().any(|p| p.contains("Nav.test.tsx")),
"Should find Nav.test.tsx via directory-level, got: {:?}",
paths
);
assert!(
paths.iter().any(|p| p.contains("NavExpandable.test.tsx")),
"Should find NavExpandable.test.tsx via directory-level, got: {:?}",
paths
);
}
#[test]
fn infer_parent_slider_step() {
assert_eq!(
infer_parent_component_name("SliderStep"),
Some("Slider".into())
);
}
#[test]
fn infer_parent_card_header() {
assert_eq!(
infer_parent_component_name("CardHeader"),
Some("Card".into())
);
}
#[test]
fn infer_parent_modal_box_title() {
assert_eq!(
infer_parent_component_name("ModalBoxTitle"),
Some("Modal".into())
);
}
#[test]
fn infer_parent_popover_header_icon() {
assert_eq!(
infer_parent_component_name("PopoverHeaderIcon"),
Some("Popover".into())
);
}
#[test]
fn infer_parent_none_for_short() {
assert_eq!(infer_parent_component_name("Ab"), None); assert_eq!(infer_parent_component_name("Icon"), None); }
#[test]
fn find_tests_parent_tests_dir() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src");
let api = src.join("api");
let tests = src.join("__tests__").join("api");
std::fs::create_dir_all(&api).unwrap();
std::fs::create_dir_all(&tests).unwrap();
std::fs::write(api.join("users.ts"), "").unwrap();
std::fs::write(tests.join("users.test.ts"), "").unwrap();
let found = find_test_files(dir.path(), Path::new("src/api/users.ts")).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(
found[0].path,
PathBuf::from("src/__tests__/api/users.test.ts")
);
}
}