use std::path::{Path, PathBuf};
use rustc_hash::FxHashSet;
use super::config_parser;
use super::{Plugin, PluginResult};
const BUILTIN_REPORTERS: &[&str] = &["default", "verbose", "summary"];
const MAX_PROJECTS_DEPTH: usize = 4;
const MAX_EXPANDED_PROJECTS: usize = 64;
const MAX_GLOB_ITERATIONS: usize = 1024;
const JEST_CONFIG_FILENAMES: &[&str] = &[
"jest.config.ts",
"jest.config.js",
"jest.config.mjs",
"jest.config.cjs",
"jest.config.json",
];
const PACKAGE_JSON_FILENAME: &str = "package.json";
define_plugin!(
struct JestPlugin => "jest",
enablers: &["jest"],
entry_patterns: &[
"***.spec.{ts,tsx,js,jsx}",
"**/__tests__*.{ts,tsx,js,jsx}",
"**/__mocks__*.{ts,tsx,js,jsx,mjs,cjs}",
],
config_patterns: &["jest.config.{ts,js,mjs,cjs}", "jest.config.json"],
always_used: &["jest.config.{ts,js,mjs,cjs}", "jest.setup.{ts,js,tsx,jsx}"],
tooling_dependencies: &["jest", "jest-environment-jsdom", "ts-jest", "babel-jest"],
fixture_glob_patterns: &[
"**/__fixtures__*.{ts,tsx,js,jsx,json}",
"**/fixtures*.{ts,tsx,js,jsx,json}",
],
package_json_config_key: "jest",
resolve_config(config_path, source, root) {
let mut result = PluginResult::default();
let mut visited = FxHashSet::default();
extract_jest_config(config_path, source, root, &mut result, &mut visited, 0);
result
},
);
/// Parse a Jest config and recurse into any `projects` entries.
///
/// The visited set is keyed by canonicalized config path so cycles
/// (`a` referencing `b` referencing `a`) terminate after one full pass
/// across each file.
fn extract_jest_config(
config_path: &Path,
source: &str,
root: &Path,
result: &mut PluginResult,
visited: &mut FxHashSet<PathBuf>,
depth: usize,
) {
let key = std::fs::canonicalize(config_path).unwrap_or_else(|_| config_path.to_path_buf());
if !visited.insert(key) {
return;
}
// Resolve the on-disk file shape into a (parse_source, parse_path) pair
// that the AST helpers can ingest. `package.json` is parsed via serde to
// pluck out the `"jest"` key, then re-serialised as a parenthesised
// expression so the helpers see the same shape they would for a
// standalone `jest.config.json`.
let filename = config_path.file_name().and_then(|n| n.to_str());
let (parse_source, parse_path_buf) = if filename == Some(PACKAGE_JSON_FILENAME) {
let Some(jest_str) = extract_package_json_jest_value(source) else {
return;
};
(format!("({jest_str})"), config_path.with_extension("js"))
} else if config_path.extension().is_some_and(|ext| ext == "json") {
(format!("({source})"), config_path.with_extension("js"))
} else {
(source.to_string(), config_path.to_path_buf())
};
let parse_path: &Path = &parse_path_buf;
// Extract import sources as referenced dependencies
let imports = config_parser::extract_imports(&parse_source, parse_path);
for imp in &imports {
let dep = crate::resolve::extract_package_name(imp);
result.referenced_dependencies.push(dep);
}
extract_jest_setup_files(&parse_source, parse_path, root, result);
extract_jest_dependencies(&parse_source, parse_path, result);
if depth >= MAX_PROJECTS_DEPTH {
return;
}
extract_jest_inline_projects(&parse_source, parse_path, root, result);
let project_entries =
config_parser::extract_config_string_array(&parse_source, parse_path, &["projects"]);
for entry in &project_entries {
for child_config in expand_project_entry(entry, config_path, root) {
let Ok(child_source) = std::fs::read_to_string(&child_config) else {
continue;
};
// Each child config carries its own <rootDir>: the child's
// own directory. Setup files are resolved against it.
let child_root = child_config
.parent()
.map_or_else(|| root.to_path_buf(), Path::to_path_buf);
// Jest's `projects` semantics scope `testMatch` / `testRegex` /
// `replace_entry_patterns` to each child individually: a narrow
// pattern in one project must not replace the parent's broad
// defaults for sibling projects. Run each child into a scratch
// result and merge only the workspace-global fields back.
let mut child_result = PluginResult::default();
extract_jest_config(
&child_config,
&child_source,
&child_root,
&mut child_result,
visited,
depth + 1,
);
result
.referenced_dependencies
.extend(child_result.referenced_dependencies);
result.setup_files.extend(child_result.setup_files);
}
}
}
/// Parse `package.json` source and return its top-level `"jest"` value
/// re-serialised as a JSON string suitable for wrapping in parentheses
/// and feeding to the existing AST-based config helpers.
fn extract_package_json_jest_value(source: &str) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(source).ok()?;
let jest_val = parsed.get("jest")?;
if !jest_val.is_object() {
// `"jest": "./jest.config.js"` (string preset path) and other
// non-object shapes are not currently followed; only the inline
// object form participates in dependency / setup-file extraction.
return None;
}
serde_json::to_string(jest_val).ok()
}
/// Credit referenced dependencies and setup files for inline
/// `ProjectConfig` objects in a `projects: [...]` array, e.g.
/// `projects: [{ preset: "ts-jest", runner: "jest-runner-eslint" }]`.
///
/// Each field follows the same rules as the top-level extraction
/// (`extract_jest_setup_files` / `extract_jest_dependencies`): built-in
/// runners / environments are filtered, relative-path resolvers are
/// dropped, and the existing `BUILTIN_REPORTERS` list applies. `transform`
/// (an object whose values are package names) is intentionally not
/// followed when it appears inline, because the existing
/// `extract_config_shallow_strings` helper only walks the top level.
///
/// All field reads route through
/// `extract_config_array_nested_string_or_array`, which inspects
/// only object elements of the array (string elements like
/// `"<rootDir>/packages*` on a deep tree of non-config files
fn expand_project_entry(entry: &str, config_path: &Path, root: &Path) -> Vec<PathBuf> {
let resolved = resolve_project_pattern(entry, config_path, root);
let pattern_str = resolved.to_string_lossy();
let mut configs = Vec::new();
let Ok(matches) = glob::glob(&pattern_str) else {
return configs;
};
let mut iterations = 0_usize;
for matched in matches.flatten() {
iterations += 1;
if iterations > MAX_GLOB_ITERATIONS || configs.len() >= MAX_EXPANDED_PROJECTS {
break;
}
if matched.is_dir() {
if let Some(found) = probe_directory_for_jest_config(&matched) {
configs.push(found);
}
} else if matched.is_file() && is_recognised_config_path(&matched) {
configs.push(matched);
}
}
configs
}
fn probe_directory_for_jest_config(dir: &Path) -> Option<PathBuf> {
for filename in JEST_CONFIG_FILENAMES {
let candidate = dir.join(filename);
if candidate.is_file() {
return Some(candidate);
}
}
let pkg = dir.join(PACKAGE_JSON_FILENAME);
if pkg.is_file() && package_json_has_jest_key(&pkg) {
return Some(pkg);
}
None
}
fn package_json_has_jest_key(path: &Path) -> bool {
let Ok(raw) = std::fs::read_to_string(path) else {
return false;
};
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&raw) else {
return false;
};
parsed.get("jest").is_some_and(serde_json::Value::is_object)
}
fn is_recognised_config_path(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
return false;
};
if JEST_CONFIG_FILENAMES.contains(&name) {
return true;
}
name == PACKAGE_JSON_FILENAME && package_json_has_jest_key(path)
}
fn resolve_project_pattern(entry: &str, config_path: &Path, root: &Path) -> PathBuf {
if let Some(rest) = entry.strip_prefix("<rootDir>") {
let trimmed = rest.trim_start_matches(['/', '\\']);
return root.join(trimmed);
}
let path = Path::new(entry);
if path.is_absolute() {
return path.to_path_buf();
}
config_path.parent().unwrap_or(root).join(entry)
}
fn extract_jest_setup_files(
parse_source: &str,
parse_path: &Path,
root: &Path,
result: &mut PluginResult,
) {
if let Some(preset) =
config_parser::extract_config_string(parse_source, parse_path, &["preset"])
{
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(&preset));
}
for key in &["setupFiles", "setupFilesAfterEnv"] {
let files = config_parser::extract_config_string_array(parse_source, parse_path, &[key]);
for f in &files {
result
.setup_files
.push(root.join(f.trim_start_matches("./")));
}
}
for key in &["globalSetup", "globalTeardown"] {
if let Some(path) = config_parser::extract_config_string(parse_source, parse_path, &[key]) {
result
.setup_files
.push(root.join(path.trim_start_matches("./")));
}
}
let test_match =
config_parser::extract_config_string_array(parse_source, parse_path, &["testMatch"]);
if !test_match.is_empty() {
result.replace_entry_patterns = true;
}
result.extend_entry_patterns(test_match);
if result.entry_patterns.is_empty()
&& let Some(regex) =
config_parser::extract_config_string(parse_source, parse_path, &["testRegex"])
&& let Some(glob) = test_regex_to_glob(®ex)
{
result.replace_entry_patterns = true;
result.push_entry_pattern(glob);
}
}
fn test_regex_to_glob(regex: &str) -> Option<String> {
let meta_chars = ['.', '*', '+', '?', '(', '[', '|', '^', '$', '{', '\\'];
let prefix_end = regex
.find(|c: char| meta_chars.contains(&c))
.unwrap_or(regex.len());
let prefix = ®ex[..prefix_end];
if prefix.is_empty() || !prefix.contains('/') {
return None;
}
let ext = if regex.contains("tsx?") {
"{ts,tsx}"
} else if regex.contains("jsx?") {
"{js,jsx}"
} else if regex.contains("\\.ts") {
"ts"
} else if regex.contains("\\.js") {
"js"
} else {
"{ts,tsx,js,jsx}"
};
let name_pattern = if regex.contains("(test|spec)") || regex.contains("(spec|test)") {
"*.{test,spec}"
} else if regex.contains("\\.spec\\.") {
"*.spec"
} else {
"*.test"
};
Some(format!("{prefix}**/{name_pattern}.{ext}"))
}
fn extract_jest_dependencies(parse_source: &str, parse_path: &Path, result: &mut PluginResult) {
let transform_values =
config_parser::extract_config_shallow_strings(parse_source, parse_path, "transform");
for val in &transform_values {
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(val));
}
let reporters =
config_parser::extract_config_shallow_strings(parse_source, parse_path, "reporters");
for reporter in &reporters {
if !BUILTIN_REPORTERS.contains(&reporter.as_str()) {
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(reporter));
}
}
if let Some(env) =
config_parser::extract_config_string(parse_source, parse_path, &["testEnvironment"])
&& !matches!(env.as_str(), "node" | "jsdom")
{
result
.referenced_dependencies
.push(format!("jest-environment-{env}"));
result.referenced_dependencies.push(env);
}
let watch_plugins =
config_parser::extract_config_shallow_strings(parse_source, parse_path, "watchPlugins");
for plugin in &watch_plugins {
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(plugin));
}
if let Some(resolver) =
config_parser::extract_config_string(parse_source, parse_path, &["resolver"])
&& !resolver.starts_with('.')
&& !resolver.starts_with('/')
{
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(&resolver));
}
let serializers = config_parser::extract_config_string_array(
parse_source,
parse_path,
&["snapshotSerializers"],
);
for s in &serializers {
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(s));
}
if let Some(runner) =
config_parser::extract_config_string(parse_source, parse_path, &["testRunner"])
&& !matches!(
runner.as_str(),
"jest-jasmine2" | "jest-circus" | "jest-circus/runner"
)
{
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(&runner));
}
if let Some(runner) =
config_parser::extract_config_string(parse_source, parse_path, &["runner"])
&& runner != "jest-runner"
{
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(&runner));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_config_preset() {
let source = r#"module.exports = { preset: "ts-jest" };"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string())
);
}
#[test]
fn resolve_config_global_setup_teardown() {
let source = r#"
module.exports = {
globalSetup: "./test/global-setup.ts",
globalTeardown: "./test/global-teardown.ts"
};
"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
result
.setup_files
.contains(&std::path::PathBuf::from("/project/test/global-setup.ts"))
);
assert!(result.setup_files.contains(&std::path::PathBuf::from(
"/project/test/global-teardown.ts"
)));
}
#[test]
fn resolve_config_watch_plugins() {
let source = r#"
module.exports = {
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
};
"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
let deps = &result.referenced_dependencies;
assert!(deps.contains(&"jest-watch-typeahead".to_string()));
}
#[test]
fn resolve_config_resolver() {
let source = r#"module.exports = { resolver: "jest-resolver-enhanced" };"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
result
.referenced_dependencies
.contains(&"jest-resolver-enhanced".to_string())
);
}
#[test]
fn resolve_config_resolver_relative_not_added() {
let source = r#"module.exports = { resolver: "./custom-resolver.js" };"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
!result
.referenced_dependencies
.iter()
.any(|d| d.contains("custom-resolver"))
);
}
#[test]
fn resolve_config_snapshot_serializers() {
let source = r#"
module.exports = {
snapshotSerializers: ["enzyme-to-json/serializer"]
};
"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
result
.referenced_dependencies
.contains(&"enzyme-to-json".to_string())
);
}
#[test]
fn resolve_config_test_runner_builtin() {
let source = r#"module.exports = { testRunner: "jest-circus/runner" };"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
!result
.referenced_dependencies
.iter()
.any(|d| d.contains("jest-circus"))
);
}
#[test]
fn resolve_config_custom_runner() {
let source = r#"module.exports = { runner: "jest-runner-eslint" };"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
result
.referenced_dependencies
.contains(&"jest-runner-eslint".to_string())
);
}
#[test]
fn resolve_config_json() {
let source = r#"{"preset": "ts-jest", "testEnvironment": "jsdom"}"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.json"),
source,
std::path::Path::new("/project"),
);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string())
);
}
#[test]
fn test_regex_with_directory_prefix() {
assert_eq!(
test_regex_to_glob(r"src/.*\.test\.ts$"),
Some("src/**/*.test.ts".to_string())
);
}
#[test]
fn test_regex_without_directory_prefix() {
assert_eq!(
test_regex_to_glob(r".*\.test\.ts$"),
None,
"regex without directory prefix should return None (same as defaults)"
);
}
#[test]
fn test_regex_tsx_extension() {
assert_eq!(
test_regex_to_glob(r"src/.*\.test\.tsx?$"),
Some("src/**/*.test.{ts,tsx}".to_string())
);
}
#[test]
fn test_regex_spec_pattern() {
assert_eq!(
test_regex_to_glob(r"src/.*\.spec\.ts$"),
Some("src/**/*.spec.ts".to_string())
);
}
#[test]
fn test_regex_test_or_spec() {
assert_eq!(
test_regex_to_glob(r"src/.*(test|spec)\.ts$"),
Some("src/**/*.{test,spec}.ts".to_string())
);
}
#[test]
fn resolve_config_test_regex_replaces_defaults() {
let source =
r#"{"testRegex": "src/.*\\.test\\.ts$", "transform": {"^.+\\.tsx?$": "ts-jest"}}"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.json"),
source,
std::path::Path::new("/project"),
);
assert!(
result.replace_entry_patterns,
"testRegex with directory prefix should trigger replacement"
);
assert_eq!(result.entry_patterns, vec!["src/**/*.test.ts"]);
}
#[test]
fn resolve_config_json_transform_object_values() {
let source = r#"{"transform": {"^.+\\.tsx?$": "ts-jest"}}"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.json"),
source,
std::path::Path::new("/project"),
);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string()),
"should extract transform values from object"
);
}
#[test]
fn resolve_config_projects_directory_glob() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let pkg_a = root.join("packages/a");
let pkg_b = root.join("packages/b");
fs::create_dir_all(&pkg_a).unwrap();
fs::create_dir_all(&pkg_b).unwrap();
fs::write(
pkg_a.join("jest.config.js"),
r#"module.exports = { preset: "ts-jest" };"#,
)
.unwrap();
fs::write(
pkg_b.join("jest.config.js"),
r#"module.exports = { preset: "babel-jest" };"#,
)
.unwrap();
let parent_source = r#"module.exports = { projects: ["<rootDir>/packages/*"] };"#;
let parent_path = root.join("jest.config.js");
fs::write(&parent_path, parent_source).unwrap();
let plugin = JestPlugin;
let result = plugin.resolve_config(&parent_path, parent_source, root);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string()),
"child a's preset must be credited, got: {:?}",
result.referenced_dependencies,
);
assert!(
result
.referenced_dependencies
.contains(&"babel-jest".to_string()),
"child b's preset must be credited, got: {:?}",
result.referenced_dependencies,
);
}
#[test]
fn resolve_config_projects_file_glob() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let pkg_a = root.join("packages/a");
fs::create_dir_all(&pkg_a).unwrap();
fs::write(
pkg_a.join("jest.config.ts"),
r#"export default { reporters: ["jest-junit"] };"#,
)
.unwrap();
let parent_source =
r#"module.exports = { projects: ["<rootDir>/packages/*/jest.config.ts"] };"#;
let parent_path = root.join("jest.config.js");
fs::write(&parent_path, parent_source).unwrap();
let plugin = JestPlugin;
let result = plugin.resolve_config(&parent_path, parent_source, root);
assert!(
result
.referenced_dependencies
.contains(&"jest-junit".to_string()),
"child reporter must be credited, got: {:?}",
result.referenced_dependencies,
);
}
#[test]
fn resolve_config_projects_missing_child_config_ignored() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let pkg = root.join("packages/empty");
fs::create_dir_all(&pkg).unwrap();
let parent_source =
r#"module.exports = { projects: ["<rootDir>/packages/*"], preset: "ts-jest" };"#;
let parent_path = root.join("jest.config.js");
fs::write(&parent_path, parent_source).unwrap();
let plugin = JestPlugin;
let result = plugin.resolve_config(&parent_path, parent_source, root);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string()),
"parent's own preset must still be credited",
);
}
#[test]
fn resolve_config_projects_cycle_guard() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let a_dir = root.join("a");
let b_dir = root.join("b");
fs::create_dir_all(&a_dir).unwrap();
fs::create_dir_all(&b_dir).unwrap();
let a_path = a_dir.join("jest.config.js");
let b_path = b_dir.join("jest.config.js");
fs::write(
&a_path,
format!(
r#"module.exports = {{ projects: ["{}"], preset: "ts-jest" }};"#,
b_path.to_string_lossy().replace('\\', "/"),
),
)
.unwrap();
fs::write(
&b_path,
format!(
r#"module.exports = {{ projects: ["{}"], preset: "babel-jest" }};"#,
a_path.to_string_lossy().replace('\\', "/"),
),
)
.unwrap();
let plugin = JestPlugin;
let source_a = std::fs::read_to_string(&a_path).unwrap();
let result = plugin.resolve_config(&a_path, &source_a, root);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string()),
);
assert!(
result
.referenced_dependencies
.contains(&"babel-jest".to_string()),
);
}
#[test]
fn resolve_config_projects_child_entry_patterns_do_not_leak_to_parent() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let pkg = root.join("packages/a");
fs::create_dir_all(&pkg).unwrap();
fs::write(
pkg.join("jest.config.js"),
r#"module.exports = { testMatch: ["src/**/only-here.test.ts"], preset: "ts-jest" };"#,
)
.unwrap();
let parent_source = r#"module.exports = { projects: ["<rootDir>/packages/*"] };"#;
let parent_path = root.join("jest.config.js");
fs::write(&parent_path, parent_source).unwrap();
let plugin = JestPlugin;
let result = plugin.resolve_config(&parent_path, parent_source, root);
assert!(
!result.replace_entry_patterns,
"child's testMatch must NOT toggle replace_entry_patterns on the parent",
);
assert!(
!result
.entry_patterns
.iter()
.any(|p| p.pattern == "src/**/only-here.test.ts"),
"child's testMatch entry pattern must NOT appear in the parent's entry list, got: {:?}",
result.entry_patterns,
);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string()),
"child's preset must still be credited via the merge",
);
}
#[test]
fn resolve_config_projects_metadata_only_inline_object_no_deps() {
let source =
r#"module.exports = { projects: [{ displayName: "foo" }, "<rootDir>/packages/a"] };"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/tmp/fallow-jest-projects-nonexistent-zzz"),
);
assert!(
result.referenced_dependencies.is_empty(),
"metadata-only field must not be credited as a dep, got: {:?}",
result.referenced_dependencies,
);
}
#[test]
fn resolve_config_projects_inline_object_credits_dependencies() {
let source = r#"
module.exports = {
projects: [
{ preset: "ts-jest", runner: "jest-runner-eslint" },
{
displayName: "browser",
testEnvironment: "jest-environment-puppeteer",
snapshotSerializers: ["enzyme-to-json/serializer"],
reporters: ["default", "jest-junit"]
},
{
setupFiles: ["./setup-browser.ts"],
globalSetup: "./global-setup.ts"
}
]
};
"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
let deps = &result.referenced_dependencies;
assert!(
deps.contains(&"ts-jest".to_string()),
"preset must be credited, got: {deps:?}",
);
assert!(
deps.contains(&"jest-runner-eslint".to_string()),
"custom runner must be credited, got: {deps:?}",
);
assert!(
deps.contains(&"jest-environment-puppeteer".to_string()),
"testEnvironment must surface as a dep, got: {deps:?}",
);
assert!(
deps.contains(&"enzyme-to-json".to_string()),
"snapshotSerializer package must be credited, got: {deps:?}",
);
assert!(
deps.contains(&"jest-junit".to_string()),
"non-builtin reporter must be credited, got: {deps:?}",
);
assert!(
!deps.iter().any(|d| d == "default"),
"builtin reporter `default` must not be credited as a dep, got: {deps:?}",
);
assert!(
result
.setup_files
.contains(&std::path::PathBuf::from("/project/setup-browser.ts")),
"inline setupFiles must be added to result.setup_files, got: {:?}",
result.setup_files,
);
assert!(
result
.setup_files
.contains(&std::path::PathBuf::from("/project/global-setup.ts")),
"inline globalSetup must be added to result.setup_files, got: {:?}",
result.setup_files,
);
}
#[test]
fn resolve_config_projects_inline_object_filters_builtins() {
let source = r#"
module.exports = {
projects: [
{ testRunner: "jest-circus", testEnvironment: "node", runner: "jest-runner" }
]
};
"#;
let plugin = JestPlugin;
let result = plugin.resolve_config(
std::path::Path::new("jest.config.js"),
source,
std::path::Path::new("/project"),
);
assert!(
!result
.referenced_dependencies
.iter()
.any(|d| d.contains("jest-circus") || d.contains("jest-runner") || d == "node"),
"built-in testRunner / runner / testEnvironment must not be credited, got: {:?}",
result.referenced_dependencies,
);
}
#[test]
fn resolve_config_projects_directory_glob_picks_up_package_json_jest_key() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let pkg = root.join("packages/a");
fs::create_dir_all(&pkg).unwrap();
fs::write(
pkg.join("package.json"),
r#"{ "name": "a", "jest": { "preset": "ts-jest" } }"#,
)
.unwrap();
let parent_source = r#"module.exports = { projects: ["<rootDir>/packages/*"] };"#;
let parent_path = root.join("jest.config.js");
fs::write(&parent_path, parent_source).unwrap();
let plugin = JestPlugin;
let result = plugin.resolve_config(&parent_path, parent_source, root);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string()),
"preset from package.json#jest must be credited, got: {:?}",
result.referenced_dependencies,
);
}
#[test]
fn resolve_config_projects_package_json_without_jest_key_skipped() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let pkg = root.join("packages/a");
fs::create_dir_all(&pkg).unwrap();
fs::write(pkg.join("package.json"), r#"{ "name": "a" }"#).unwrap();
let parent_source =
r#"module.exports = { projects: ["<rootDir>/packages/*"], preset: "ts-jest" };"#;
let parent_path = root.join("jest.config.js");
fs::write(&parent_path, parent_source).unwrap();
let plugin = JestPlugin;
let result = plugin.resolve_config(&parent_path, parent_source, root);
assert!(
result
.referenced_dependencies
.contains(&"ts-jest".to_string()),
);
assert_eq!(
result.referenced_dependencies.len(),
1,
"no extra deps should be credited from a package.json with no jest key, got: {:?}",
result.referenced_dependencies,
);
}
}