feat: add comprehensive testing and error tracking for edit mode
Add robust testing framework to prevent regression of edit mode: - Comprehensive regression tests for JavaScript syntax validation - Build-time JavaScript validation tool with Node.js integration - Enhanced error tracking with detailed logging and recovery - Makefile integration for `make validate-js` command Features: - Validates JavaScript syntax in generated HTML templates - Detects common issues like broken string literals and brace escaping - Enhanced error reporting with timestamps and context - Automatic error recovery for graceful degradation - Build validation to catch syntax errors before deployment 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
283
tools/validate_js_syntax.py
Executable file
283
tools/validate_js_syntax.py
Executable file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
JavaScript syntax validation tool for MarkiTect build process.
|
||||
|
||||
This tool validates that all generated JavaScript in edit mode is syntactically correct,
|
||||
preventing the regression that caused edit mode to fail completely.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
|
||||
class JavaScriptValidator:
|
||||
"""Validates JavaScript syntax in MarkiTect templates."""
|
||||
|
||||
def __init__(self):
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
|
||||
def validate_document_manager_templates(self) -> bool:
|
||||
"""Validate JavaScript templates in DocumentManager."""
|
||||
try:
|
||||
# Import the template generation method directly to avoid database dependency
|
||||
from markitect.document_manager import DocumentManager
|
||||
|
||||
# Create a mock DocumentManager to access the template method
|
||||
class MockDatabaseManager:
|
||||
pass
|
||||
|
||||
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||
doc_manager.database_manager = MockDatabaseManager()
|
||||
|
||||
# Test various configurations
|
||||
test_cases = [
|
||||
{
|
||||
'title': 'Basic Edit Mode',
|
||||
'markdown': '# Test\n\nBasic content.',
|
||||
'edit_mode': True,
|
||||
'theme': 'github'
|
||||
},
|
||||
{
|
||||
'title': 'Complex Content',
|
||||
'markdown': '''# Header 1
|
||||
|
||||
## Header 2
|
||||
|
||||
Paragraph with **bold** and *italic* text.
|
||||
|
||||
- List item 1
|
||||
- List item 2
|
||||
|
||||
1. Numbered item
|
||||
2. Another item
|
||||
|
||||
> Blockquote text
|
||||
|
||||
```python
|
||||
code block
|
||||
```
|
||||
|
||||
Final paragraph.''',
|
||||
'edit_mode': True,
|
||||
'theme': 'dark'
|
||||
},
|
||||
{
|
||||
'title': 'Special Characters',
|
||||
'markdown': "# Test 'quotes' and \"double quotes\"\n\nContent with $pecial ch@racters & symbols!",
|
||||
'edit_mode': True,
|
||||
'theme': 'github'
|
||||
}
|
||||
]
|
||||
|
||||
all_valid = True
|
||||
|
||||
for test_case in test_cases:
|
||||
print(f"Validating: {test_case['title']}")
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title=test_case['title'],
|
||||
markdown_content=test_case['markdown'],
|
||||
edit_mode=test_case['edit_mode'],
|
||||
editor_theme=test_case.get('theme', 'github')
|
||||
)
|
||||
|
||||
is_valid, errors = self._validate_html_javascript(html_content, test_case['title'])
|
||||
|
||||
if not is_valid:
|
||||
all_valid = False
|
||||
self.errors.extend(errors)
|
||||
else:
|
||||
print(f" ✅ {test_case['title']} - JavaScript syntax valid")
|
||||
|
||||
return all_valid
|
||||
|
||||
except Exception as e:
|
||||
self.errors.append(f"Failed to validate document manager templates: {e}")
|
||||
return False
|
||||
|
||||
def _validate_html_javascript(self, html_content: str, context: str) -> Tuple[bool, List[str]]:
|
||||
"""Extract and validate JavaScript from HTML content."""
|
||||
errors = []
|
||||
|
||||
# Extract all JavaScript blocks
|
||||
js_blocks = re.findall(r'<script[^>]*>(.*?)</script>', html_content, re.DOTALL)
|
||||
|
||||
if not js_blocks:
|
||||
errors.append(f"{context}: No JavaScript blocks found")
|
||||
return False, errors
|
||||
|
||||
for i, js_content in enumerate(js_blocks):
|
||||
# Skip empty blocks or blocks with only external src
|
||||
if not js_content.strip() or 'src=' in js_content:
|
||||
continue
|
||||
|
||||
is_valid, js_errors = self._validate_javascript_syntax(js_content, f"{context} block {i+1}")
|
||||
if not is_valid:
|
||||
errors.extend(js_errors)
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def _validate_javascript_syntax(self, js_content: str, context: str) -> Tuple[bool, List[str]]:
|
||||
"""Validate JavaScript syntax using Node.js."""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
||||
f.write(js_content)
|
||||
temp_js_path = f.name
|
||||
|
||||
# Use Node.js to check syntax
|
||||
result = subprocess.run(
|
||||
['node', '-c', temp_js_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
errors.append(f"{context}: JavaScript syntax error - {result.stderr.strip()}")
|
||||
|
||||
# Add helpful debugging info
|
||||
lines = js_content.split('\n')
|
||||
for line_num, line in enumerate(lines[:20], 1): # Show first 20 lines
|
||||
if line.strip():
|
||||
print(f" Line {line_num}: {line[:100]}") # First 100 chars
|
||||
|
||||
Path(temp_js_path).unlink(missing_ok=True)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
errors.append(f"{context}: JavaScript validation timeout")
|
||||
except FileNotFoundError:
|
||||
errors.append(f"{context}: Node.js not available for validation")
|
||||
except Exception as e:
|
||||
errors.append(f"{context}: Validation error - {e}")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def check_common_issues(self) -> bool:
|
||||
"""Check for common JavaScript issues that have caused problems."""
|
||||
try:
|
||||
from markitect.document_manager import DocumentManager
|
||||
|
||||
# Create a mock DocumentManager to access the template method
|
||||
class MockDatabaseManager:
|
||||
pass
|
||||
|
||||
doc_manager = DocumentManager.__new__(DocumentManager)
|
||||
doc_manager.database_manager = MockDatabaseManager()
|
||||
|
||||
html_content = doc_manager._generate_html_template(
|
||||
title="Issue Check",
|
||||
markdown_content="# Test\n\nTest content.",
|
||||
edit_mode=True
|
||||
)
|
||||
|
||||
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
|
||||
if not js_match:
|
||||
self.errors.append("No JavaScript found for common issues check")
|
||||
return False
|
||||
|
||||
js_content = js_match.group(1)
|
||||
issues_found = False
|
||||
|
||||
# Check for specific issues that have caused problems
|
||||
issue_patterns = [
|
||||
(r"'\s*\n\s*'", "Broken string literal across lines"),
|
||||
(r'"\s*\n\s*"', "Broken string literal across lines"),
|
||||
(r'} else if.*{{.*}} else if.*{[^{]', "Inconsistent brace escaping"),
|
||||
(r'\$\{\{.*?\}\}', "Double-escaped template literals"),
|
||||
(r'reconstructed \+= .*\'\n', "Unescaped newline in string concatenation"),
|
||||
]
|
||||
|
||||
for pattern, description in issue_patterns:
|
||||
matches = re.findall(pattern, js_content)
|
||||
if matches:
|
||||
self.errors.append(f"Found {description}: {matches[:3]}...") # Show first 3 matches
|
||||
issues_found = True
|
||||
|
||||
# Check for required elements
|
||||
required_elements = [
|
||||
'MarkitectEditor',
|
||||
'updateStatus',
|
||||
'makeContentEditable',
|
||||
'DOMContentLoaded'
|
||||
]
|
||||
|
||||
for element in required_elements:
|
||||
if element not in js_content:
|
||||
self.errors.append(f"Missing required element: {element}")
|
||||
issues_found = True
|
||||
|
||||
return not issues_found
|
||||
|
||||
except Exception as e:
|
||||
self.errors.append(f"Failed to check common issues: {e}")
|
||||
return False
|
||||
|
||||
def validate_all(self) -> bool:
|
||||
"""Run all validation checks."""
|
||||
print("🔍 Validating JavaScript syntax in MarkiTect templates...")
|
||||
|
||||
all_checks_passed = True
|
||||
|
||||
# Run all validation checks
|
||||
checks = [
|
||||
("Document Manager Templates", self.validate_document_manager_templates),
|
||||
("Common Issues", self.check_common_issues),
|
||||
]
|
||||
|
||||
for check_name, check_func in checks:
|
||||
print(f"\n📋 Running {check_name} check...")
|
||||
try:
|
||||
if not check_func():
|
||||
all_checks_passed = False
|
||||
print(f" ❌ {check_name} check failed")
|
||||
else:
|
||||
print(f" ✅ {check_name} check passed")
|
||||
except Exception as e:
|
||||
all_checks_passed = False
|
||||
self.errors.append(f"{check_name} check failed with exception: {e}")
|
||||
print(f" ❌ {check_name} check failed with exception")
|
||||
|
||||
return all_checks_passed
|
||||
|
||||
def report_results(self) -> None:
|
||||
"""Print validation results."""
|
||||
print("\n" + "="*60)
|
||||
print("JavaScript Validation Results")
|
||||
print("="*60)
|
||||
|
||||
if self.errors:
|
||||
print(f"\n❌ {len(self.errors)} Error(s) Found:")
|
||||
for i, error in enumerate(self.errors, 1):
|
||||
print(f" {i}. {error}")
|
||||
|
||||
if self.warnings:
|
||||
print(f"\n⚠️ {len(self.warnings)} Warning(s):")
|
||||
for i, warning in enumerate(self.warnings, 1):
|
||||
print(f" {i}. {warning}")
|
||||
|
||||
if not self.errors and not self.warnings:
|
||||
print("\n✅ All JavaScript validation checks passed!")
|
||||
|
||||
print("\n" + "="*60)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main validation function."""
|
||||
validator = JavaScriptValidator()
|
||||
|
||||
success = validator.validate_all()
|
||||
validator.report_results()
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = main()
|
||||
sys.exit(exit_code)
|
||||
Reference in New Issue
Block a user