feat: bundle JUnit XML reporter for Jest, replacing external jest-junit dependency
Ship a zero-dependency jest-reporter.js inside the codeflash runtime package instead of requiring the external jest-junit npm package. This ensures the reporter is always available when codeflash is installed, fixing Jest-based projects (Strapi, Moleculer) that failed because jest-junit wasn't installed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
86202d40e5
commit
3c2a2b3694
4 changed files with 510 additions and 6 deletions
|
|
@ -42,6 +42,12 @@ def clear_created_config_files() -> None:
|
|||
_created_config_files.clear()
|
||||
|
||||
|
||||
# The bundled JUnit reporter path, resolved as "codeflash/jest-reporter"
|
||||
# This is shipped inside the codeflash npm runtime package, so it's always
|
||||
# available when the codeflash runtime is installed (which is already required).
|
||||
CODEFLASH_JEST_REPORTER = "codeflash/jest-reporter"
|
||||
|
||||
|
||||
def _detect_bundler_module_resolution(project_root: Path) -> bool:
|
||||
"""Detect if the project uses moduleResolution: 'bundler' in tsconfig.
|
||||
|
||||
|
|
@ -698,7 +704,7 @@ def run_jest_behavioral_tests(
|
|||
"npx",
|
||||
"jest",
|
||||
"--reporters=default",
|
||||
"--reporters=jest-junit",
|
||||
f"--reporters={CODEFLASH_JEST_REPORTER}",
|
||||
"--runInBand", # Run tests serially for consistent timing
|
||||
"--forceExit",
|
||||
]
|
||||
|
|
@ -732,7 +738,7 @@ def run_jest_behavioral_tests(
|
|||
jest_env["JEST_JUNIT_OUTPUT_FILE"] = str(result_file_path)
|
||||
jest_env["JEST_JUNIT_OUTPUT_DIR"] = str(result_file_path.parent)
|
||||
jest_env["JEST_JUNIT_OUTPUT_NAME"] = result_file_path.name
|
||||
# Configure jest-junit to use filepath-based classnames for proper parsing
|
||||
# Configure codeflash jest-reporter to use filepath-based classnames for proper parsing
|
||||
jest_env["JEST_JUNIT_CLASSNAME"] = "{filepath}"
|
||||
jest_env["JEST_JUNIT_SUITE_NAME"] = "{filepath}"
|
||||
jest_env["JEST_JUNIT_ADD_FILE_ATTRIBUTE"] = "true"
|
||||
|
|
@ -797,7 +803,7 @@ def run_jest_behavioral_tests(
|
|||
except FileNotFoundError:
|
||||
logger.error("Jest not found. Make sure Jest is installed (npm install jest)")
|
||||
result = subprocess.CompletedProcess(
|
||||
args=jest_cmd, returncode=-1, stdout="", stderr="Jest not found. Run: npm install jest jest-junit"
|
||||
args=jest_cmd, returncode=-1, stdout="", stderr="Jest not found. Run: npm install jest"
|
||||
)
|
||||
finally:
|
||||
wall_clock_ns = time.perf_counter_ns() - start_time_ns
|
||||
|
|
@ -947,7 +953,7 @@ def run_jest_benchmarking_tests(
|
|||
"npx",
|
||||
"jest",
|
||||
"--reporters=default",
|
||||
"--reporters=jest-junit",
|
||||
f"--reporters={CODEFLASH_JEST_REPORTER}",
|
||||
"--runInBand", # Ensure serial execution
|
||||
"--forceExit",
|
||||
"--runner=codeflash/loop-runner", # Use custom loop runner for in-process looping
|
||||
|
|
@ -1113,7 +1119,7 @@ def run_jest_line_profile_tests(
|
|||
"npx",
|
||||
"jest",
|
||||
"--reporters=default",
|
||||
"--reporters=jest-junit",
|
||||
f"--reporters={CODEFLASH_JEST_REPORTER}",
|
||||
"--runInBand", # Run tests serially for consistent line profiling
|
||||
"--forceExit",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "codeflash",
|
||||
"version": "0.8.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Codeflash - AI-powered code optimization for JavaScript and TypeScript",
|
||||
"main": "runtime/index.js",
|
||||
"types": "runtime/index.d.ts",
|
||||
|
|
@ -32,6 +32,10 @@
|
|||
"./loop-runner": {
|
||||
"require": "./runtime/loop-runner.js",
|
||||
"import": "./runtime/loop-runner.js"
|
||||
},
|
||||
"./jest-reporter": {
|
||||
"require": "./runtime/jest-reporter.js",
|
||||
"import": "./runtime/jest-reporter.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
204
packages/codeflash/runtime/jest-reporter.js
Normal file
204
packages/codeflash/runtime/jest-reporter.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
/**
|
||||
* Codeflash JUnit XML Reporter for Jest.
|
||||
*
|
||||
* Minimal reporter that outputs JUnit XML in the format expected by
|
||||
* codeflash's Python parser. Replaces the external jest-junit dependency.
|
||||
*
|
||||
* Configuration via environment variables (same as jest-junit):
|
||||
* JEST_JUNIT_OUTPUT_FILE — absolute path for the XML file (required)
|
||||
* JEST_JUNIT_CLASSNAME — template for classname ("{filepath}" supported)
|
||||
* JEST_JUNIT_SUITE_NAME — template for suite name ("{filepath}" supported)
|
||||
* JEST_JUNIT_ADD_FILE_ATTRIBUTE — "true" to add file= on <testcase>
|
||||
* JEST_JUNIT_INCLUDE_CONSOLE_OUTPUT — "true" to include console.log in <system-out>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function escapeXml(str) {
|
||||
if (!str) return "";
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function escapeXmlContent(str) {
|
||||
if (!str) return "";
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function formatTemplate(template, values) {
|
||||
if (!template) return "";
|
||||
let result = template;
|
||||
for (const [key, val] of Object.entries(values)) {
|
||||
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val || "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class CodeflashJestReporter {
|
||||
constructor(globalConfig, _reporterOptions) {
|
||||
this._globalConfig = globalConfig;
|
||||
this._outputFile = process.env.JEST_JUNIT_OUTPUT_FILE || "jest-results.xml";
|
||||
this._classnameTemplate = process.env.JEST_JUNIT_CLASSNAME || "{classname}";
|
||||
this._suiteNameTemplate = process.env.JEST_JUNIT_SUITE_NAME || "{filepath}";
|
||||
this._addFileAttribute =
|
||||
process.env.JEST_JUNIT_ADD_FILE_ATTRIBUTE === "true";
|
||||
this._includeConsoleOutput =
|
||||
process.env.JEST_JUNIT_INCLUDE_CONSOLE_OUTPUT === "true";
|
||||
// Capture buffered console output per test file
|
||||
this._consoleBuffers = new Map();
|
||||
}
|
||||
|
||||
// Called by Jest when a test suite starts — we just note it
|
||||
onTestStart(_test) {}
|
||||
|
||||
// Called by Jest with console output for a test file
|
||||
onTestFileResult(_test, testResult, _aggregatedResult) {
|
||||
if (
|
||||
this._includeConsoleOutput &&
|
||||
testResult.console &&
|
||||
testResult.console.length > 0
|
||||
) {
|
||||
const messages = testResult.console
|
||||
.map((entry) => {
|
||||
const prefix =
|
||||
entry.type === "error"
|
||||
? "console.error"
|
||||
: entry.type === "warn"
|
||||
? "console.warn"
|
||||
: "console.log";
|
||||
return `${prefix}\n ${entry.message}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
this._consoleBuffers.set(testResult.testFilePath, messages);
|
||||
}
|
||||
}
|
||||
|
||||
onRunComplete(_testContexts, results) {
|
||||
const suites = [];
|
||||
let totalTests = 0;
|
||||
let totalFailures = 0;
|
||||
let totalErrors = 0;
|
||||
let totalTime = 0;
|
||||
|
||||
for (const suiteResult of results.testResults) {
|
||||
const filePath = suiteResult.testFilePath || "";
|
||||
const relativePath = this._globalConfig.rootDir
|
||||
? path.relative(this._globalConfig.rootDir, filePath)
|
||||
: filePath;
|
||||
|
||||
const templateVars = {
|
||||
filepath: filePath,
|
||||
filename: path.basename(filePath),
|
||||
classname: relativePath.replace(/\//g, ".").replace(/\.[^.]+$/, ""),
|
||||
title: "",
|
||||
displayName: suiteResult.displayName || "",
|
||||
};
|
||||
|
||||
const suiteName = formatTemplate(
|
||||
this._suiteNameTemplate,
|
||||
templateVars
|
||||
);
|
||||
|
||||
const testcases = [];
|
||||
let suiteFailures = 0;
|
||||
let suiteErrors = 0;
|
||||
let suiteTime = 0;
|
||||
|
||||
for (const testResult of suiteResult.testResults) {
|
||||
const duration = (testResult.duration || 0) / 1000; // ms → seconds
|
||||
suiteTime += duration;
|
||||
|
||||
const tcTemplateVars = {
|
||||
...templateVars,
|
||||
title: testResult.fullName || testResult.title || "",
|
||||
};
|
||||
|
||||
const classname = formatTemplate(
|
||||
this._classnameTemplate,
|
||||
tcTemplateVars
|
||||
);
|
||||
|
||||
let tcXml = ` <testcase classname="${escapeXml(classname)}" name="${escapeXml(
|
||||
testResult.fullName || testResult.title
|
||||
)}" time="${duration.toFixed(3)}"`;
|
||||
|
||||
if (this._addFileAttribute) {
|
||||
tcXml += ` file="${escapeXml(filePath)}"`;
|
||||
}
|
||||
|
||||
if (
|
||||
testResult.status === "failed" &&
|
||||
testResult.failureMessages &&
|
||||
testResult.failureMessages.length > 0
|
||||
) {
|
||||
suiteFailures++;
|
||||
const failureText = testResult.failureMessages.join("\n");
|
||||
tcXml += `>\n <failure message="${escapeXml(
|
||||
failureText.split("\n")[0]
|
||||
)}"><![CDATA[${failureText}]]></failure>\n </testcase>`;
|
||||
} else if (testResult.status === "pending") {
|
||||
tcXml += `>\n <skipped/>\n </testcase>`;
|
||||
} else {
|
||||
tcXml += "/>";
|
||||
}
|
||||
|
||||
testcases.push(tcXml);
|
||||
}
|
||||
|
||||
totalTests += suiteResult.testResults.length;
|
||||
totalFailures += suiteFailures;
|
||||
totalErrors += suiteErrors;
|
||||
totalTime += suiteTime;
|
||||
|
||||
// Build suite XML
|
||||
let suiteXml = ` <testsuite name="${escapeXml(suiteName)}" tests="${
|
||||
suiteResult.testResults.length
|
||||
}" errors="${suiteErrors}" failures="${suiteFailures}" skipped="${
|
||||
suiteResult.testResults.filter((t) => t.status === "pending").length
|
||||
}" timestamp="${new Date().toISOString()}" time="${suiteTime.toFixed(3)}"`;
|
||||
|
||||
if (this._addFileAttribute) {
|
||||
suiteXml += ` file="${escapeXml(filePath)}"`;
|
||||
}
|
||||
|
||||
suiteXml += ">\n";
|
||||
suiteXml += testcases.join("\n") + "\n";
|
||||
|
||||
// Add console output as system-out (at suite level, matching jest-junit format)
|
||||
if (this._includeConsoleOutput) {
|
||||
const consoleOutput =
|
||||
this._consoleBuffers.get(suiteResult.testFilePath) || "";
|
||||
if (consoleOutput) {
|
||||
suiteXml += ` <system-out><![CDATA[${consoleOutput}]]></system-out>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
suiteXml += " </testsuite>";
|
||||
suites.push(suiteXml);
|
||||
}
|
||||
|
||||
const xml = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
`<testsuites name="jest tests" tests="${totalTests}" failures="${totalFailures}" errors="${totalErrors}" time="${totalTime.toFixed(3)}">`,
|
||||
...suites,
|
||||
"</testsuites>",
|
||||
].join("\n");
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.dirname(this._outputFile);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(this._outputFile, xml, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodeflashJestReporter;
|
||||
|
|
@ -728,3 +728,293 @@ class TestBundlerModuleResolutionFix:
|
|||
# Verify codeflash configs were NOT created
|
||||
assert not (tmpdir_path / "jest.codeflash.config.js").exists()
|
||||
assert not (tmpdir_path / "tsconfig.codeflash.json").exists()
|
||||
|
||||
|
||||
class TestBundledJestReporter:
|
||||
"""Tests for the bundled codeflash/jest-reporter.
|
||||
|
||||
Verifies that:
|
||||
1. The reporter JS file exists in the runtime package
|
||||
2. Jest commands reference 'codeflash/jest-reporter' (not jest-junit)
|
||||
3. The reporter produces valid JUnit XML
|
||||
4. The CODEFLASH_JEST_REPORTER constant is correct
|
||||
"""
|
||||
|
||||
def test_reporter_js_file_exists(self):
|
||||
"""The jest-reporter.js file must exist in the runtime directory."""
|
||||
reporter_path = Path(__file__).resolve().parents[2] / "packages" / "codeflash" / "runtime" / "jest-reporter.js"
|
||||
assert reporter_path.exists(), f"jest-reporter.js not found at {reporter_path}"
|
||||
|
||||
def test_reporter_constant_value(self):
|
||||
"""CODEFLASH_JEST_REPORTER should be 'codeflash/jest-reporter'."""
|
||||
from codeflash.languages.javascript.test_runner import CODEFLASH_JEST_REPORTER
|
||||
|
||||
assert CODEFLASH_JEST_REPORTER == "codeflash/jest-reporter"
|
||||
|
||||
def test_behavioral_command_uses_bundled_reporter(self):
|
||||
"""run_jest_behavioral_tests should use codeflash/jest-reporter in --reporters flag."""
|
||||
from codeflash.languages.javascript.test_runner import run_jest_behavioral_tests
|
||||
from codeflash.models.models import TestFile, TestFiles
|
||||
from codeflash.models.test_type import TestType
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir_path = Path(tmpdir)
|
||||
(tmpdir_path / "package.json").write_text('{"name": "test"}')
|
||||
test_dir = tmpdir_path / "test"
|
||||
test_dir.mkdir()
|
||||
test_file = test_dir / "test_func.test.js"
|
||||
test_file.write_text("// test")
|
||||
|
||||
mock_test_files = TestFiles(
|
||||
test_files=[
|
||||
TestFile(
|
||||
original_file_path=test_file,
|
||||
instrumented_behavior_file_path=test_file,
|
||||
benchmarking_file_path=test_file,
|
||||
test_type=TestType.GENERATED_REGRESSION,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = ""
|
||||
mock_result.returncode = 1
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
try:
|
||||
run_jest_behavioral_tests(
|
||||
test_paths=mock_test_files,
|
||||
test_env={},
|
||||
cwd=tmpdir_path,
|
||||
project_root=tmpdir_path,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mock_run.called:
|
||||
cmd = mock_run.call_args[0][0]
|
||||
reporter_args = [a for a in cmd if "--reporters=" in a and "jest-reporter" in a]
|
||||
assert len(reporter_args) == 1, f"Expected exactly one codeflash/jest-reporter flag, got: {reporter_args}"
|
||||
assert reporter_args[0] == "--reporters=codeflash/jest-reporter"
|
||||
# Must NOT reference jest-junit
|
||||
jest_junit_args = [a for a in cmd if "jest-junit" in a]
|
||||
assert len(jest_junit_args) == 0, f"Should not reference jest-junit: {jest_junit_args}"
|
||||
|
||||
def test_benchmarking_command_uses_bundled_reporter(self):
|
||||
"""run_jest_benchmarking_tests should use codeflash/jest-reporter."""
|
||||
from codeflash.languages.javascript.test_runner import run_jest_benchmarking_tests
|
||||
from codeflash.models.models import TestFile, TestFiles
|
||||
from codeflash.models.test_type import TestType
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir_path = Path(tmpdir)
|
||||
(tmpdir_path / "package.json").write_text('{"name": "test"}')
|
||||
test_dir = tmpdir_path / "test"
|
||||
test_dir.mkdir()
|
||||
test_file = test_dir / "test_func__perf.test.js"
|
||||
test_file.write_text("// test")
|
||||
|
||||
mock_test_files = TestFiles(
|
||||
test_files=[
|
||||
TestFile(
|
||||
original_file_path=test_file,
|
||||
instrumented_behavior_file_path=test_file,
|
||||
benchmarking_file_path=test_file,
|
||||
test_type=TestType.GENERATED_REGRESSION,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = ""
|
||||
mock_result.returncode = 1
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
try:
|
||||
run_jest_benchmarking_tests(
|
||||
test_paths=mock_test_files,
|
||||
test_env={},
|
||||
cwd=tmpdir_path,
|
||||
project_root=tmpdir_path,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mock_run.called:
|
||||
cmd = mock_run.call_args[0][0]
|
||||
reporter_args = [a for a in cmd if "--reporters=codeflash/jest-reporter" in a]
|
||||
assert len(reporter_args) == 1
|
||||
|
||||
def test_line_profile_command_uses_bundled_reporter(self):
|
||||
"""run_jest_line_profile_tests should use codeflash/jest-reporter."""
|
||||
from codeflash.languages.javascript.test_runner import run_jest_line_profile_tests
|
||||
from codeflash.models.models import TestFile, TestFiles
|
||||
from codeflash.models.test_type import TestType
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir_path = Path(tmpdir)
|
||||
(tmpdir_path / "package.json").write_text('{"name": "test"}')
|
||||
test_dir = tmpdir_path / "test"
|
||||
test_dir.mkdir()
|
||||
test_file = test_dir / "test_func__line.test.js"
|
||||
test_file.write_text("// test")
|
||||
|
||||
mock_test_files = TestFiles(
|
||||
test_files=[
|
||||
TestFile(
|
||||
original_file_path=test_file,
|
||||
instrumented_behavior_file_path=test_file,
|
||||
benchmarking_file_path=test_file,
|
||||
test_type=TestType.GENERATED_REGRESSION,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = ""
|
||||
mock_result.returncode = 1
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
try:
|
||||
run_jest_line_profile_tests(
|
||||
test_paths=mock_test_files,
|
||||
test_env={},
|
||||
cwd=tmpdir_path,
|
||||
project_root=tmpdir_path,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mock_run.called:
|
||||
cmd = mock_run.call_args[0][0]
|
||||
reporter_args = [a for a in cmd if "--reporters=codeflash/jest-reporter" in a]
|
||||
assert len(reporter_args) == 1
|
||||
|
||||
def test_reporter_produces_valid_junit_xml(self):
|
||||
"""The reporter JS should produce JUnit XML parseable by junitparser."""
|
||||
import subprocess
|
||||
|
||||
reporter_path = Path(__file__).resolve().parents[2] / "packages" / "codeflash" / "runtime" / "jest-reporter.js"
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_file = Path(tmpdir) / "results.xml"
|
||||
|
||||
# Create a Node.js script that exercises the reporter with mock data
|
||||
test_script = Path(tmpdir) / "test_reporter.js"
|
||||
test_script.write_text(f"""
|
||||
// Set env vars BEFORE requiring reporter (matches real Jest behavior)
|
||||
process.env.JEST_JUNIT_OUTPUT_FILE = '{output_file}';
|
||||
process.env.JEST_JUNIT_CLASSNAME = '{{filepath}}';
|
||||
process.env.JEST_JUNIT_SUITE_NAME = '{{filepath}}';
|
||||
process.env.JEST_JUNIT_ADD_FILE_ATTRIBUTE = 'true';
|
||||
process.env.JEST_JUNIT_INCLUDE_CONSOLE_OUTPUT = 'true';
|
||||
|
||||
const Reporter = require('{reporter_path}');
|
||||
|
||||
// Mock Jest globalConfig
|
||||
const globalConfig = {{ rootDir: '/tmp/project' }};
|
||||
const reporter = new Reporter(globalConfig, {{}});
|
||||
|
||||
// Mock test results (matches Jest's aggregatedResults structure)
|
||||
const results = {{
|
||||
testResults: [
|
||||
{{
|
||||
testFilePath: '/tmp/project/test/math.test.js',
|
||||
displayName: 'math tests',
|
||||
console: [{{ type: 'log', message: 'CODEFLASH_START test1' }}],
|
||||
testResults: [
|
||||
{{
|
||||
fullName: 'math > adds numbers',
|
||||
title: 'adds numbers',
|
||||
status: 'passed',
|
||||
duration: 12,
|
||||
}},
|
||||
{{
|
||||
fullName: 'math > handles failure',
|
||||
title: 'handles failure',
|
||||
status: 'failed',
|
||||
duration: 5,
|
||||
failureMessages: ['Expected 4 but got 5'],
|
||||
}},
|
||||
{{
|
||||
fullName: 'math > skipped test',
|
||||
title: 'skipped test',
|
||||
status: 'pending',
|
||||
duration: 0,
|
||||
}},
|
||||
],
|
||||
}},
|
||||
],
|
||||
}};
|
||||
|
||||
// Simulate onTestFileResult for console capture
|
||||
reporter.onTestFileResult(null, results.testResults[0], null);
|
||||
|
||||
// Simulate onRunComplete
|
||||
reporter.onRunComplete([], results);
|
||||
|
||||
console.log('OK');
|
||||
""")
|
||||
|
||||
result = subprocess.run(
|
||||
["node", str(test_script)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Reporter script failed: {result.stderr}"
|
||||
assert output_file.exists(), "Reporter did not create output file"
|
||||
|
||||
xml_content = output_file.read_text()
|
||||
|
||||
# Verify basic XML structure
|
||||
assert '<?xml version="1.0"' in xml_content
|
||||
assert "<testsuites" in xml_content
|
||||
assert "<testsuite" in xml_content
|
||||
assert "<testcase" in xml_content
|
||||
|
||||
# Verify classname uses filepath template
|
||||
assert 'classname="/tmp/project/test/math.test.js"' in xml_content
|
||||
|
||||
# Verify file attribute is present
|
||||
assert 'file="/tmp/project/test/math.test.js"' in xml_content
|
||||
|
||||
# Verify failure element
|
||||
assert "<failure" in xml_content
|
||||
assert "Expected 4 but got 5" in xml_content
|
||||
|
||||
# Verify skipped element
|
||||
assert "<skipped/>" in xml_content
|
||||
|
||||
# Verify system-out with console output
|
||||
assert "<system-out>" in xml_content
|
||||
assert "CODEFLASH_START" in xml_content
|
||||
|
||||
# Verify it's parseable by junitparser (our actual parser)
|
||||
from junitparser import JUnitXml
|
||||
|
||||
parsed = JUnitXml.fromfile(str(output_file))
|
||||
suites = list(parsed)
|
||||
assert len(suites) == 1
|
||||
testcases = list(suites[0])
|
||||
assert len(testcases) == 3
|
||||
|
||||
def test_reporter_export_in_package_json(self):
|
||||
"""package.json should export codeflash/jest-reporter."""
|
||||
import json
|
||||
|
||||
pkg_path = Path(__file__).resolve().parents[2] / "packages" / "codeflash" / "package.json"
|
||||
with pkg_path.open() as f:
|
||||
pkg = json.load(f)
|
||||
|
||||
exports = pkg.get("exports", {})
|
||||
assert "./jest-reporter" in exports, "Missing ./jest-reporter export in package.json"
|
||||
assert exports["./jest-reporter"]["require"] == "./runtime/jest-reporter.js"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue