generated from coulomb/repo-seed
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>
321 lines
10 KiB
Python
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(),
|
|
} |