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>
283 lines
9.7 KiB
Python
Executable File
283 lines
9.7 KiB
Python
Executable File
#!/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) |