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>
This commit is contained in:
2025-12-16 00:01:58 +01:00
parent 7ef23c2905
commit 9d7964f9e5
70 changed files with 32933 additions and 1702 deletions

View File

@@ -0,0 +1,18 @@
"""
TestDrive-JSUI Capability
A comprehensive JavaScript UI testing framework capability for MarkiTect.
Provides tools for testing, developing, and maintaining JavaScript UI components
with seamless Python integration.
"""
__version__ = "0.1.0"
__author__ = "MarkiTect Project"
from .testing.js_test_runner import JavaScriptTestRunner
from .testing.integration import PythonJSBridge
__all__ = [
"JavaScriptTestRunner",
"PythonJSBridge",
]

View File

@@ -0,0 +1,3 @@
"""
JavaScript UI components and widgets.
"""

View File

@@ -0,0 +1,3 @@
"""
Core JavaScript UI framework components.
"""

View File

@@ -0,0 +1,13 @@
"""
JavaScript UI testing integration components.
"""
from .js_test_runner import JavaScriptTestRunner, JSTestResult
from .integration import PythonJSBridge, discover_js_tests
__all__ = [
'JavaScriptTestRunner',
'JSTestResult',
'PythonJSBridge',
'discover_js_tests'
]

View File

@@ -0,0 +1,187 @@
"""
Python-JavaScript Integration Bridge
Provides seamless integration between Python test suite and JavaScript tests.
Enables pytest to discover and run JavaScript tests as if they were Python tests.
"""
import pytest
from pathlib import Path
from typing import List, Optional, Generator
from .js_test_runner import JavaScriptTestRunner, JSTestResult
class PythonJSBridge:
"""
Bridge between Python and JavaScript testing environments.
Enables JavaScript tests to be run from Python test suite.
"""
def __init__(self, capability_root: Optional[Path] = None):
"""
Initialize the bridge.
Args:
capability_root: Root directory of the testdrive-jsui capability
"""
self.js_runner = JavaScriptTestRunner(capability_root)
def pytest_collect_file(self, path: Path, parent) -> Optional["JSTestFile"]:
"""
Pytest hook to collect JavaScript test files.
"""
if path.suffix == ".js" and "test" in path.name:
if self._is_js_test_file(path):
return JSTestFile.from_parent(parent, fspath=path)
return None
def _is_js_test_file(self, path: Path) -> bool:
"""Check if a file is a JavaScript test file."""
return (
path.name.startswith("test-") or
path.name.endswith(".test.js") or
"test" in path.parts
)
def run_all_js_tests(self) -> JSTestResult:
"""Run all JavaScript tests."""
return self.js_runner.run_js_tests()
def run_js_test_by_name(self, test_name: str) -> JSTestResult:
"""Run a specific JavaScript test by name."""
return self.js_runner.run_specific_test(test_name)
class JSTestFile(pytest.File):
"""
Represents a JavaScript test file in pytest.
"""
def collect(self) -> Generator["JSTestItem", None, None]:
"""Collect test items from this JavaScript file."""
# For now, treat each JS file as a single test item
# In the future, this could be enhanced to parse individual test functions
yield JSTestItem.from_parent(self, name=self.fspath.basename)
class JSTestItem(pytest.Item):
"""
Represents a JavaScript test item in pytest.
"""
def __init__(self, name: str, parent: JSTestFile):
super().__init__(name, parent)
self.js_runner = JavaScriptTestRunner()
def runtest(self) -> None:
"""Run the JavaScript test."""
test_file = str(self.fspath.relative_to(self.js_runner.js_dir))
result = self.js_runner.run_specific_test(test_file)
if not result.success:
failure_messages = []
for failure in result.failures:
failure_messages.append(f"{failure.get('title', 'Unknown')}: {failure.get('message', 'Unknown error')}")
failure_msg = "\n".join(failure_messages) if failure_messages else result.stderr
raise JSTestFailure(failure_msg, result)
def repr_failure(self, excinfo) -> str:
"""Represent test failure."""
if isinstance(excinfo.value, JSTestFailure):
return f"JavaScript test failed:\n{excinfo.value.message}"
return super().repr_failure(excinfo)
def reportinfo(self):
"""Report information about this test item."""
return self.fspath, 0, f"JavaScript test: {self.name}"
class JSTestFailure(Exception):
"""Exception raised when a JavaScript test fails."""
def __init__(self, message: str, result: JSTestResult):
super().__init__(message)
self.message = message
self.result = result
# Pytest fixtures for JavaScript testing
@pytest.fixture
def js_test_runner() -> JavaScriptTestRunner:
"""Provide a JavaScript test runner instance."""
return JavaScriptTestRunner()
@pytest.fixture
def js_bridge() -> PythonJSBridge:
"""Provide a Python-JavaScript bridge instance."""
return PythonJSBridge()
# Pytest markers
def pytest_configure(config):
"""Configure pytest markers for JavaScript tests."""
config.addinivalue_line(
"markers", "javascript: mark test as JavaScript integration test"
)
config.addinivalue_line(
"markers", "js_component: mark test as JavaScript component test"
)
config.addinivalue_line(
"markers", "js_integration: mark test as JavaScript integration test"
)
# Test discovery function for manual use
def discover_js_tests(capability_root: Optional[Path] = None) -> List[str]:
"""
Discover all JavaScript test files.
Args:
capability_root: Root directory of the testdrive-jsui capability
Returns:
List of JavaScript test file paths
"""
runner = JavaScriptTestRunner(capability_root)
return runner.list_available_tests()
# Main test execution functions
def test_javascript_components(js_test_runner: JavaScriptTestRunner) -> None:
"""
Main test function that runs all JavaScript component tests.
This can be called from Python test suite.
"""
result = js_test_runner.run_js_tests(verbose=True)
assert result.success, f"JavaScript tests failed: {result.failures}"
assert result.tests_total > 0, "No JavaScript tests were found or executed"
assert result.tests_passed > 0, "No JavaScript tests passed"
def test_javascript_integration(js_test_runner: JavaScriptTestRunner) -> None:
"""
Test JavaScript integration components.
"""
integration_tests = [
test for test in js_test_runner.list_available_tests()
if "integration" in test.lower()
]
if integration_tests:
result = js_test_runner.run_js_tests(test_patterns=integration_tests)
assert result.success, f"JavaScript integration tests failed: {result.failures}"
def test_javascript_environment(js_test_runner: JavaScriptTestRunner) -> None:
"""
Test that JavaScript testing environment is properly set up.
"""
assert js_test_runner.check_node_environment(), "Node.js environment not available"
info = js_test_runner.get_test_info()
assert info["node_available"], "Node.js not available"
assert info["package_json_exists"], "package.json not found"
assert len(info["available_tests"]) > 0, "No JavaScript tests found"

View File

@@ -0,0 +1,321 @@
"""
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(),
}

View File

@@ -0,0 +1,3 @@
"""
Python test wrappers for JavaScript tests.
"""

View File

@@ -0,0 +1,3 @@
"""
JavaScript UI utility functions and helpers.
"""