Files
testdrive-jsui/src/testdrive_jsui/testing/js_test_runner.py
tegwick 9d7964f9e5 feat: add refactored testdrive-jsui capability with consolidated architecture
Complete integration of refactored testdrive-jsui capability:

## Refactored Architecture
- js/ - All JavaScript source (controls, components, core)
- static/ - CSS, images, templates
- src/testdrive_jsui/ - Python package
- tests/ - Python tests

## Plugin Self-Declaration
- get_plugin_source_dir() - plugin declares own location
- get_asset_paths() - organized asset paths
- No hardcoded discovery logic

## Merged Content
- Baseline UI scaffold (tutorials, LICENSE, INTRODUCTION.md)
- Refactored capability implementation
- Comprehensive documentation

Ready for standalone use or integration with markitect.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 00:01:58 +01:00

321 lines
10 KiB
Python

"""
JavaScript Test Runner
Provides integration between Python test suite and JavaScript tests.
Allows running JavaScript tests from Python and collecting results.
"""
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional, Union, Any
from dataclasses import dataclass
import tempfile
import os
@dataclass
class JSTestResult:
"""Results from running JavaScript tests."""
success: bool
tests_passed: int
tests_failed: int
tests_total: int
failures: List[Dict[str, Any]]
coverage: Optional[Dict[str, Any]] = None
duration: float = 0.0
stdout: str = ""
stderr: str = ""
class JavaScriptTestRunner:
"""
Runs JavaScript tests via Node.js and Jest, integrating with Python test suite.
"""
def __init__(self, capability_root: Optional[Path] = None):
"""
Initialize the JavaScript test runner.
Args:
capability_root: Root directory of the testdrive-jsui capability.
If None, attempts to auto-discover.
"""
self.capability_root = capability_root or self._find_capability_root()
self.js_dir = self.capability_root / "js"
self.package_json = self.capability_root / "package.json"
if not self.package_json.exists():
raise ValueError(f"package.json not found at {self.package_json}")
def _find_capability_root(self) -> Path:
"""Auto-discover the capability root directory."""
current = Path(__file__).parent
while current.parent != current:
if (current / "package.json").exists():
return current
current = current.parent
# Fallback: look for capabilities directory structure
current = Path(__file__).parent
while current.parent != current:
capabilities_dir = current / "capabilities" / "testdrive-jsui"
if capabilities_dir.exists():
return capabilities_dir
current = current.parent
raise ValueError("Could not find testdrive-jsui capability root")
def check_node_environment(self) -> bool:
"""
Check if Node.js and npm are available.
Returns:
True if Node.js environment is ready, False otherwise.
"""
try:
# Check Node.js
subprocess.run(
["node", "--version"],
capture_output=True,
check=True,
cwd=self.capability_root
)
# Check npm
subprocess.run(
["npm", "--version"],
capture_output=True,
check=True,
cwd=self.capability_root
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def install_dependencies(self) -> bool:
"""
Install JavaScript dependencies via npm.
Returns:
True if installation succeeded, False otherwise.
"""
try:
result = subprocess.run(
["npm", "install"],
capture_output=True,
text=True,
check=True,
cwd=self.capability_root
)
return result.returncode == 0
except subprocess.CalledProcessError:
return False
def run_js_tests(
self,
test_patterns: Optional[List[str]] = None,
coverage: bool = False,
verbose: bool = False
) -> JSTestResult:
"""
Run JavaScript tests via Jest.
Args:
test_patterns: Specific test files or patterns to run
coverage: Whether to collect coverage information
verbose: Whether to run in verbose mode
Returns:
JSTestResult containing test execution results
"""
if not self.check_node_environment():
return JSTestResult(
success=False,
tests_passed=0,
tests_failed=1,
tests_total=1,
failures=[{"message": "Node.js environment not available"}],
stderr="Node.js or npm not found"
)
# Build Jest command
cmd = ["npm", "test", "--"]
if coverage:
cmd.append("--coverage")
if verbose:
cmd.append("--verbose")
# Add JSON reporter for parsing results
cmd.extend(["--json", "--outputFile", "test-results.json"])
if test_patterns:
cmd.extend(test_patterns)
try:
# Run the tests
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=self.capability_root,
timeout=300 # 5 minute timeout
)
# Parse JSON results
test_results_file = self.capability_root / "test-results.json"
if test_results_file.exists():
try:
with open(test_results_file, 'r') as f:
jest_results = json.load(f)
# Clean up results file
test_results_file.unlink()
return self._parse_jest_results(jest_results, result)
except (json.JSONDecodeError, KeyError) as e:
return JSTestResult(
success=False,
tests_passed=0,
tests_failed=1,
tests_total=1,
failures=[{"message": f"Failed to parse test results: {e}"}],
stdout=result.stdout,
stderr=result.stderr
)
else:
# Fallback: parse from stdout/stderr
return self._parse_output_results(result)
except subprocess.TimeoutExpired:
return JSTestResult(
success=False,
tests_passed=0,
tests_failed=1,
tests_total=1,
failures=[{"message": "Test execution timed out"}],
stderr="Test execution exceeded 5 minute timeout"
)
except Exception as e:
return JSTestResult(
success=False,
tests_passed=0,
tests_failed=1,
tests_total=1,
failures=[{"message": f"Test execution failed: {e}"}],
stderr=str(e)
)
def _parse_jest_results(self, jest_results: Dict, process_result) -> JSTestResult:
"""Parse Jest JSON results into JSTestResult."""
success = jest_results.get("success", False)
# Extract test counts
num_passed_tests = jest_results.get("numPassedTests", 0)
num_failed_tests = jest_results.get("numFailedTests", 0)
num_total_tests = jest_results.get("numTotalTests", 0)
# Extract failures
failures = []
test_results = jest_results.get("testResults", [])
for test_file in test_results:
for assertion in test_file.get("assertionResults", []):
if assertion.get("status") == "failed":
failures.append({
"title": assertion.get("title", "Unknown test"),
"message": assertion.get("failureMessages", ["Unknown failure"])[0],
"file": test_file.get("name", "Unknown file")
})
return JSTestResult(
success=success,
tests_passed=num_passed_tests,
tests_failed=num_failed_tests,
tests_total=num_total_tests,
failures=failures,
duration=jest_results.get("startTime", 0),
stdout=process_result.stdout,
stderr=process_result.stderr
)
def _parse_output_results(self, process_result) -> JSTestResult:
"""Fallback: parse results from stdout/stderr."""
success = process_result.returncode == 0
stdout = process_result.stdout
# Simple parsing for basic stats
tests_passed = 0
tests_failed = 0
if "passed" in stdout:
try:
# Look for pattern like "5 passed"
import re
passed_match = re.search(r"(\d+)\s+passed", stdout)
if passed_match:
tests_passed = int(passed_match.group(1))
failed_match = re.search(r"(\d+)\s+failed", stdout)
if failed_match:
tests_failed = int(failed_match.group(1))
except (ValueError, AttributeError):
pass
return JSTestResult(
success=success,
tests_passed=tests_passed,
tests_failed=tests_failed,
tests_total=tests_passed + tests_failed,
failures=[{"message": process_result.stderr}] if not success else [],
stdout=stdout,
stderr=process_result.stderr
)
def run_specific_test(self, test_file: str) -> JSTestResult:
"""
Run a specific JavaScript test file.
Args:
test_file: Path to the test file relative to js/tests/
Returns:
JSTestResult containing test execution results
"""
return self.run_js_tests(test_patterns=[test_file])
def list_available_tests(self) -> List[str]:
"""
List all available JavaScript test files.
Returns:
List of test file paths
"""
tests_dir = self.js_dir / "tests"
if not tests_dir.exists():
return []
test_files = []
for test_file in tests_dir.glob("**/*.js"):
if test_file.name.startswith("test-") or test_file.name.endswith(".test.js"):
test_files.append(str(test_file.relative_to(tests_dir)))
return sorted(test_files)
def get_test_info(self) -> Dict[str, Any]:
"""
Get information about the JavaScript test environment.
Returns:
Dictionary with test environment information
"""
return {
"capability_root": str(self.capability_root),
"js_directory": str(self.js_dir),
"node_available": self.check_node_environment(),
"available_tests": self.list_available_tests(),
"package_json_exists": self.package_json.exists(),
}