Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 67 additions & 15 deletions codeflash/languages/javascript/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,11 @@ def _create_codeflash_jest_config(
logger.debug("ts-jest not found in project dependencies, skipping transform config")

# Create a wrapper Jest config
if original_jest_config:
# TypeScript configs (.ts) cannot be required from CommonJS modules
# because Node.js cannot parse TypeScript syntax in require().
# When the original config is TypeScript, we create a standalone config
# instead of trying to extend it via require().
if original_jest_config and original_jest_config.suffix != ".ts":
# Since codeflash config is in the same directory as original, use simple relative path
config_require_path = f"./{original_jest_config.name}"

Expand All @@ -305,6 +309,10 @@ def _create_codeflash_jest_config(

module.exports = {{
...originalConfig,
// Disable globalSetup and globalTeardown to avoid external dependencies (e.g., Docker)
// Codeflash-generated tests should be self-contained and not require global setup/teardown
globalSetup: undefined,
globalTeardown: undefined,
// Transform ESM packages that don't work with Jest's default config
// Pattern handles both npm/yarn (node_modules/pkg) and pnpm (node_modules/.pnpm/pkg@version/node_modules/pkg)
transformIgnorePatterns: [
Expand All @@ -320,6 +328,9 @@ def _create_codeflash_jest_config(
testEnvironment: 'node',
testRegex: '\\\\.(test|spec)\\\\.(js|ts|tsx)$',
testPathIgnorePatterns: ['/dist/'],
// Disable globalSetup and globalTeardown to avoid external dependencies
globalSetup: undefined,
globalTeardown: undefined,
// Transform ESM packages that don't work with Jest's default config
// Pattern handles both npm/yarn and pnpm directory structures
transformIgnorePatterns: [
Expand Down Expand Up @@ -382,12 +393,19 @@ def _create_runtime_jest_config(base_config_path: Path | None, project_root: Pat
else:
module_dirs_line_no_base = ""

if base_config_path:
# TypeScript configs (.ts) cannot be required from CommonJS modules
# because Node.js cannot parse TypeScript syntax in require().
# When the base config is TypeScript, we create a standalone config
# instead of trying to extend it via require().
if base_config_path and base_config_path.suffix != ".ts":
require_path = f"./{base_config_path.name}"
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
const baseConfig = require('{require_path}');
module.exports = {{
...baseConfig,
// Disable globalSetup and globalTeardown to avoid external dependencies (e.g., Docker)
globalSetup: undefined,
globalTeardown: undefined,
roots: [
...(baseConfig.roots || [__dirname]),
{test_dirs_js},
Expand All @@ -399,6 +417,9 @@ def _create_runtime_jest_config(base_config_path: Path | None, project_root: Pat
else:
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
module.exports = {{
// Disable globalSetup and globalTeardown to avoid external dependencies
globalSetup: undefined,
globalTeardown: undefined,
roots: ['{project_root}', {test_dirs_js}],
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.test.tsx', '**/*.test.jsx'],
{module_dirs_line_no_base}}};
Expand All @@ -421,9 +442,9 @@ def _create_runtime_jest_config(base_config_path: Path | None, project_root: Pat
def _get_jest_config_for_project(project_root: Path) -> Path | None:
"""Get the appropriate Jest config for the project.

If the project uses bundler moduleResolution, creates and returns a
codeflash-compatible Jest config. Otherwise, returns the project's
existing Jest config.
Always creates a codeflash-compatible Jest config that disables globalSetup/globalTeardown
and handles ESM packages. If the project uses bundler moduleResolution, also creates a
compatible tsconfig.

Args:
project_root: Root of the project.
Expand All @@ -440,11 +461,15 @@ def _get_jest_config_for_project(project_root: Path) -> Path | None:
logger.info("Detected bundler moduleResolution - creating compatible config")
# Create codeflash-compatible tsconfig
_create_codeflash_tsconfig(project_root)
# Create codeflash Jest config that uses it
codeflash_jest_config = _create_codeflash_jest_config(project_root, original_jest_config)
if codeflash_jest_config:
return codeflash_jest_config

# Always create codeflash Jest config to disable globalSetup/globalTeardown
# and handle ESM packages properly
codeflash_jest_config = _create_codeflash_jest_config(project_root, original_jest_config)
if codeflash_jest_config:
logger.debug(f"Using codeflash Jest config: {codeflash_jest_config}")
return codeflash_jest_config

# Fallback to original if codeflash config creation failed
return original_jest_config


Expand Down Expand Up @@ -769,8 +794,21 @@ def run_jest_behavioral_tests(

# Get test files to run
test_files = [str(file.instrumented_behavior_file_path) for file in test_paths.test_files]
# Use provided project_root, or detect it as fallback
if project_root is None and test_files:
# In monorepos with --all mode, test_cfg.js_project_root may point to the wrong package
# (e.g., optimizing worker functions but project_root is set to server package).
# Detect the correct package from test file location to ensure Jest uses the right config.
if test_files and project_root:
first_test_file = Path(test_files[0])
detected_root = find_node_project_root(first_test_file)
# Only override if: (1) detected a different package root, (2) it has package.json,
# (3) both are peer packages (same parent directory)
if (detected_root and detected_root != project_root and
(detected_root / "package.json").exists() and
detected_root.parent == project_root.parent):
logger.debug(f"Monorepo: overriding project_root {project_root} with detected {detected_root}")
project_root = detected_root
elif project_root is None and test_files:
# Fallback: if no project_root provided, detect from test file
first_test_file = Path(test_files[0])
project_root = find_node_project_root(first_test_file)

Expand Down Expand Up @@ -1024,8 +1062,15 @@ def run_jest_benchmarking_tests(

# Get performance test files
test_files = [str(file.benchmarking_file_path) for file in test_paths.test_files if file.benchmarking_file_path]
# Use provided project_root, or detect it as fallback
if project_root is None and test_files:
# In monorepos, detect correct package from test file location
if test_files and project_root:
first_test_file = Path(test_files[0])
detected_root = find_node_project_root(first_test_file)
if (detected_root and detected_root != project_root and
(detected_root / "package.json").exists() and
detected_root.parent == project_root.parent):
project_root = detected_root
elif project_root is None and test_files:
first_test_file = Path(test_files[0])
project_root = find_node_project_root(first_test_file)

Expand Down Expand Up @@ -1198,8 +1243,15 @@ def run_jest_line_profile_tests(
elif file.benchmarking_file_path:
test_files.append(str(file.benchmarking_file_path))

# Use provided project_root, or detect it as fallback
if project_root is None and test_files:
# In monorepos, detect correct package from test file location
if test_files and project_root:
first_test_file = Path(test_files[0])
detected_root = find_node_project_root(first_test_file)
if (detected_root and detected_root != project_root and
(detected_root / "package.json").exists() and
detected_root.parent == project_root.parent):
project_root = detected_root
elif project_root is None and test_files:
first_test_file = Path(test_files[0])
project_root = find_node_project_root(first_test_file)

Expand Down
171 changes: 171 additions & 0 deletions tests/languages/javascript/test_globalsetup_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from pathlib import Path

from codeflash.languages.javascript.test_runner import (
_create_codeflash_jest_config,
_create_runtime_jest_config,
)


def test_disables_globalsetup_and_globalteardown(tmp_path: Path) -> None:
project_root = tmp_path.resolve()

original_config = project_root / "jest.config.js"
original_config.write_text(
"""
module.exports = {
testEnvironment: 'node',
globalSetup: './globalSetup.ts',
globalTeardown: './globalTeardown.ts',
setupFilesAfterEnv: ['./setupTests.js'],
};
""",
encoding="utf-8",
)

codeflash_config = _create_codeflash_jest_config(
project_root=project_root,
original_jest_config=original_config,
for_esm=False,
)

assert codeflash_config is not None
assert codeflash_config.exists()

config_content = codeflash_config.read_text(encoding="utf-8")

assert "globalSetup: undefined" in config_content
assert "globalTeardown: undefined" in config_content

# The original scripts are not embedded in the wrapper config (spread at runtime)
assert "./globalSetup.ts" not in config_content
assert "./globalTeardown.ts" not in config_content


def test_disables_globalsetup_in_minimal_config(tmp_path: Path) -> None:
project_root = tmp_path.resolve()

codeflash_config = _create_codeflash_jest_config(
project_root=project_root,
original_jest_config=None,
for_esm=False,
)

assert codeflash_config is not None
assert codeflash_config.exists()

config_content = codeflash_config.read_text(encoding="utf-8")

assert "globalSetup: undefined" in config_content
assert "globalTeardown: undefined" in config_content


def test_preserves_setupfilesafterenv(tmp_path: Path) -> None:
project_root = tmp_path.resolve()

original_config = project_root / "jest.config.js"
original_config.write_text(
"""
module.exports = {
testEnvironment: 'node',
globalSetup: './globalSetup.ts',
setupFilesAfterEnv: ['./setupTests.js'],
};
""",
encoding="utf-8",
)

codeflash_config = _create_codeflash_jest_config(
project_root=project_root,
original_jest_config=original_config,
for_esm=False,
)

assert codeflash_config is not None

config_content = codeflash_config.read_text(encoding="utf-8")

assert "globalSetup: undefined" in config_content
assert "setupFilesAfterEnv: undefined" not in config_content


def test_runtime_config_disables_globalsetup_with_base_config(tmp_path: Path) -> None:
project_root = tmp_path.resolve()

base_config = project_root / "jest.config.js"
base_config.write_text(
"""
module.exports = {
testEnvironment: 'node',
globalSetup: './globalSetup.ts',
};
""",
encoding="utf-8",
)

test_dirs = {str(project_root / "tests")}
runtime_config = _create_runtime_jest_config(
base_config_path=base_config,
project_root=project_root,
test_dirs=test_dirs,
)

assert runtime_config is not None
assert runtime_config.exists()

config_content = runtime_config.read_text(encoding="utf-8")

assert "globalSetup: undefined" in config_content
assert "globalTeardown: undefined" in config_content


def test_runtime_config_disables_globalsetup_standalone(tmp_path: Path) -> None:
project_root = tmp_path.resolve()

test_dirs = {str(project_root / "tests")}
runtime_config = _create_runtime_jest_config(
base_config_path=None,
project_root=project_root,
test_dirs=test_dirs,
)

assert runtime_config is not None
assert runtime_config.exists()

config_content = runtime_config.read_text(encoding="utf-8")

assert "globalSetup: undefined" in config_content
assert "globalTeardown: undefined" in config_content


def test_runtime_config_disables_globalsetup_with_typescript_base(tmp_path: Path) -> None:
project_root = tmp_path.resolve()

base_config = project_root / "jest.config.ts"
base_config.write_text(
"""
import { Config } from "jest";
export default {
testEnvironment: 'node',
globalSetup: './globalSetup.ts',
} as Config;
""",
encoding="utf-8",
)

test_dirs = {str(project_root / "tests")}
runtime_config = _create_runtime_jest_config(
base_config_path=base_config,
project_root=project_root,
test_dirs=test_dirs,
)

assert runtime_config is not None
assert runtime_config.exists()

config_content = runtime_config.read_text(encoding="utf-8")

assert "globalSetup: undefined" in config_content
assert "globalTeardown: undefined" in config_content

# Should NOT try to require the TypeScript config
assert "require" not in config_content
Loading