diff --git a/codeflash/languages/javascript/test_runner.py b/codeflash/languages/javascript/test_runner.py index a7ba0a974..32f9977d8 100644 --- a/codeflash/languages/javascript/test_runner.py +++ b/codeflash/languages/javascript/test_runner.py @@ -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}" @@ -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: [ @@ -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: [ @@ -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}, @@ -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}}}; @@ -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. @@ -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 @@ -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) @@ -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) @@ -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) diff --git a/tests/languages/javascript/test_globalsetup_handling.py b/tests/languages/javascript/test_globalsetup_handling.py new file mode 100644 index 000000000..6453c6359 --- /dev/null +++ b/tests/languages/javascript/test_globalsetup_handling.py @@ -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