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:
2025-10-25 16:53:09 +02:00
parent 1022e2597f
commit 64d1606740
4 changed files with 774 additions and 10 deletions

View File

@@ -83,6 +83,7 @@ help:
@echo " status - Show git status for repo and submodules"
@echo " clean - Clean build artifacts"
@echo " check-deps - Check dependency status"
@echo " validate-js - Validate JavaScript syntax in templates"
@echo ""
@echo "Documentation:"
@echo " update-digest - Update ProjectStatusDigest.md (requires Claude Code)"
@@ -1340,3 +1341,13 @@ cost-help:
@echo "💰 Currency: Costs calculated in USD and EUR"
@echo "🤖 Model: Default claude-sonnet-4 pricing"
# JavaScript validation for edit mode templates
validate-js: $(VENV)/bin/activate
@echo "🔍 Validating JavaScript syntax in templates..."
@if command -v node >/dev/null 2>&1; then \
$(PYTHON) tools/validate_js_syntax.py; \
else \
echo "⚠️ Node.js not available - skipping JavaScript validation"; \
echo " Install Node.js to enable JavaScript syntax checking"; \
fi

View File

@@ -659,18 +659,115 @@ class DocumentManager:
// Define editor class first (if in edit mode)
{editor_scripts if edit_mode else ''}
// Error reporting utility
function reportEditModeError(errorMsg, technicalDetails) {{
// Enhanced error reporting utility
function reportEditModeError(errorMsg, technicalDetails, errorType = 'error') {{
const statusDiv = document.getElementById('markitect-status');
const errorDiv = document.getElementById('error-details');
const errorText = document.getElementById('error-text');
const statusMsg = document.getElementById('status-message');
const browserInfo = document.getElementById('browser-info');
if (statusMsg) statusMsg.textContent = 'Edit mode unavailable - content displayed in read-only mode';
// Log to console for debugging
console.error('[MarkiTect Edit Mode Error]', errorMsg, technicalDetails);
// Create error report object
const errorReport = {{
timestamp: new Date().toISOString(),
error: errorMsg,
details: technicalDetails,
type: errorType,
userAgent: navigator.userAgent,
url: window.location.href,
markdownContent: typeof markdownContent !== 'undefined' ? markdownContent.length + ' chars' : 'unavailable'
}};
// Store error for potential reporting
if (!window.markitectErrors) window.markitectErrors = [];
window.markitectErrors.push(errorReport);
// Update UI
if (statusMsg) {{
const statusText = errorType === 'warning'
? 'Edit mode partially available - some features may not work'
: 'Edit mode unavailable - content displayed in read-only mode';
statusMsg.textContent = statusText;
}}
if (errorDiv) errorDiv.style.display = 'block';
if (errorText) errorText.textContent = errorMsg + (technicalDetails ? ' (' + technicalDetails + ')' : '');
if (errorText) {{
const fullError = errorMsg + (technicalDetails ? ' (' + technicalDetails + ')' : '');
errorText.textContent = fullError;
}}
if (browserInfo) browserInfo.textContent = navigator.userAgent.split(' ').slice(-2).join(' ');
// Auto-hide warnings after 10 seconds
if (errorType === 'warning' && errorDiv) {{
setTimeout(() => {{
errorDiv.style.display = 'none';
}}, 10000);
}}
}}
// Enhanced error recovery utility
function attemptErrorRecovery(error, context) {{
console.warn('[MarkiTect] Attempting error recovery for:', context, error);
try {{
// Try to ensure content is still visible
const contentDiv = document.getElementById('markdown-content');
if (contentDiv && !contentDiv.innerHTML.trim()) {{
// Fallback content rendering
const fallbackHtml = markdownContent
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/\\n\\n/g, '<br><br>')
.replace(/\\n/g, '<br>');
contentDiv.innerHTML = '<div style="white-space: pre-wrap;">' + fallbackHtml + '</div>';
reportEditModeError('Recovered with fallback rendering', 'Edit features disabled', 'warning');
return true;
}}
}} catch (recoveryError) {{
console.error('[MarkiTect] Recovery failed:', recoveryError);
}}
return false;
}}
// Validation utility for edit mode state
function validateEditModeState() {{
const issues = [];
// Check required elements
if (!document.getElementById('markdown-content')) {{
issues.push('Missing markdown-content container');
}}
if (!document.getElementById('markitect-status')) {{
issues.push('Missing status display');
}}
// Check JavaScript dependencies
if (typeof marked === 'undefined') {{
issues.push('marked.js library not available');
}}
if (typeof MARKITECT_EDIT_MODE === 'undefined') {{
issues.push('Edit mode configuration missing');
}}
// Check for MarkitectEditor
if (typeof MarkitectEditor === 'undefined') {{
issues.push('MarkitectEditor class not defined');
}}
if (issues.length > 0) {{
console.warn('[MarkiTect] Edit mode validation issues:', issues);
reportEditModeError('Edit mode validation failed', issues.join(', '), 'warning');
return false;
}}
return true;
}}
// Status update utility
@@ -718,19 +815,44 @@ class DocumentManager:
}}
// Step 2: Try to enhance with edit capabilities (if in edit mode)
{'''if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) {
{'''if (typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) {{
updateStatus("Initializing edit capabilities...");
try {
// Validate edit mode prerequisites
if (!validateEditModeState()) {{
if (!attemptErrorRecovery('validation failed', 'edit mode prerequisites')) {{
return; // Stop here if recovery fails
}}
}}
try {{
updateStatus("Creating editor instance...");
markitectEditor = new MarkitectEditor();
updateStatus("✓ Edit mode active - click any section to edit");
console.log("✓ Edit mode initialized successfully");
} catch (error) {
// Final validation check
setTimeout(() => {{
const sections = document.querySelectorAll('.markitect-section-editable');
if (sections.length === 0) {{
reportEditModeError('No editable sections found', 'Content may not be compatible with edit mode', 'warning');
}} else {{
console.log(`[MarkiTect] Found ${{sections.length}} editable sections`);
}}
}}, 1000);
}} catch (error) {{
updateStatus("Edit mode failed to initialize", true);
reportEditModeError("Edit mode initialization failed", error.message);
console.error("Edit mode error:", error);
}
}''' if edit_mode else ''}
// Try error recovery
if (attemptErrorRecovery(error, 'editor initialization')) {{
reportEditModeError("Edit mode partially recovered", error.message, 'warning');
}} else {{
reportEditModeError("Edit mode initialization failed", error.message);
}}
}}
}}''' if edit_mode else ''}
}});
// Handle CDN loading errors

View File

@@ -0,0 +1,348 @@
"""
Test suite for md-render --edit functionality to prevent regression.
This test suite specifically targets the critical JavaScript syntax errors
that were causing edit mode to fail completely, ensuring they never happen again.
"""
import tempfile
import pytest
from pathlib import Path
import re
import subprocess
class TestEditModeRegression:
"""Tests to prevent regression of the md-render --edit functionality."""
def test_edit_mode_generates_valid_javascript(self):
"""Test that edit mode generates syntactically valid JavaScript."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
# Test markdown content
test_content = "# Test Header\n\nThis is a test paragraph.\n\n## Section 2\n\nAnother paragraph."
# Generate HTML with edit mode
html_content = doc_manager._generate_html_template(
title="Test Document",
markdown_content=test_content,
edit_mode=True,
editor_theme='github',
keyboard_shortcuts=True
)
# Extract JavaScript from HTML
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
assert js_match, "No JavaScript found in edit mode HTML"
js_content = js_match.group(1)
# Write to temp file and validate syntax with Node.js
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
f.write(js_content)
temp_js_path = f.name
try:
# Use Node.js to check JavaScript syntax
result = subprocess.run(
['node', '-c', temp_js_path],
capture_output=True,
text=True
)
assert result.returncode == 0, f"JavaScript syntax error: {result.stderr}"
finally:
Path(temp_js_path).unlink()
def test_edit_mode_contains_required_functions(self):
"""Test that edit mode HTML contains all required JavaScript functions."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test",
edit_mode=True
)
# Check for critical functions that must be present
required_functions = [
'MarkitectEditor',
'updateStatus',
'reportEditModeError',
'makeContentEditable',
'handleSectionClick',
'editSection'
]
for func_name in required_functions:
assert func_name in html_content, f"Required function '{func_name}' not found in edit mode HTML"
def test_edit_mode_no_broken_string_literals(self):
"""Test that there are no broken string literals in the generated JavaScript."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test",
edit_mode=True
)
# Extract JavaScript
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
js_content = js_match.group(1)
# Check for broken string patterns that caused the original bug
broken_patterns = [
r"'\s*\n\s*'", # Broken string literal across lines
r'"\s*\n\s*"', # Broken string literal across lines
r'reconstructed \+= .*\'\n', # Unescaped newline in string
]
for pattern in broken_patterns:
matches = re.findall(pattern, js_content)
assert not matches, f"Found broken string pattern: {pattern} - matches: {matches}"
def test_edit_mode_proper_brace_escaping(self):
"""Test that braces are properly escaped in f-string templates."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test",
edit_mode=True
)
# Extract JavaScript
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
js_content = js_match.group(1)
# Check for inconsistent brace patterns
inconsistent_patterns = [
r'} else if.*{{', # Mixed single and double braces
r'}} else if.*{[^{]', # Mixed double and single braces
]
for pattern in inconsistent_patterns:
matches = re.findall(pattern, js_content)
assert not matches, f"Found inconsistent brace pattern: {pattern}"
def test_edit_mode_template_literal_syntax(self):
"""Test that template literals are properly escaped."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test",
edit_mode=True
)
# Extract JavaScript
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
js_content = js_match.group(1)
# Check for problematic template literal patterns
# Should NOT find double-escaped template literals like ${{
problematic_patterns = [
r'\$\{\{.*?\}\}', # Double-escaped template literals
]
for pattern in problematic_patterns:
matches = re.findall(pattern, js_content)
assert not matches, f"Found problematic template literal: {pattern}"
def test_edit_mode_contains_content_div(self):
"""Test that edit mode HTML contains the markdown-content div."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test Content",
edit_mode=True
)
# Should contain the content container
assert 'id="markdown-content"' in html_content
assert 'MARKITECT_EDIT_MODE = true' in html_content
assert 'markitect-edit-mode' in html_content
def test_edit_mode_error_handling_elements(self):
"""Test that edit mode includes proper error handling UI elements."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test",
edit_mode=True
)
# Should contain error handling elements
assert 'id="markitect-status"' in html_content
assert 'id="status-message"' in html_content
assert 'id="error-details"' in html_content
assert 'reportEditModeError' in html_content
def test_edit_mode_vs_normal_mode_differences(self):
"""Test that edit mode and normal mode generate different output appropriately."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
test_content = "# Test Header\n\nTest content."
# Generate both modes
normal_html = doc_manager._generate_html_template(
title="Test",
markdown_content=test_content,
edit_mode=False
)
edit_html = doc_manager._generate_html_template(
title="Test",
markdown_content=test_content,
edit_mode=True
)
# Edit mode should have additional elements
assert len(edit_html) > len(normal_html)
assert 'MarkitectEditor' in edit_html
assert 'MarkitectEditor' not in normal_html
assert 'markitect-edit-mode' in edit_html
assert 'markitect-edit-mode' not in normal_html
def test_edit_mode_javascript_execution_flow(self):
"""Test the logical flow of JavaScript execution in edit mode."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test",
edit_mode=True
)
# Extract JavaScript
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
js_content = js_match.group(1)
# Check for proper execution flow elements
flow_elements = [
'DOMContentLoaded', # Event listener setup
'MARKITECT_EDIT_MODE', # Mode check
'new MarkitectEditor', # Editor instantiation
'makeContentEditable', # Content enhancement
'handleSectionClick' # Interaction handler
]
for element in flow_elements:
assert element in js_content, f"Missing execution flow element: {element}"
def test_newline_escaping_in_javascript_strings(self):
"""Test that newlines in JavaScript strings are properly escaped."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test\n\nMultiple\nLines",
edit_mode=True
)
# Extract JavaScript
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
js_content = js_match.group(1)
# Look for the specific section that was broken
# Should find properly escaped newlines like '\\n\\n'
assert '\\\\n\\\\n' in js_content, "Newlines not properly escaped in JavaScript strings"
# Should NOT find unescaped newlines in string contexts
# This regex looks for string concatenation with actual newlines
broken_newline_pattern = r"'\s*\+\s*text\s*\+\s*'\s*\n"
matches = re.findall(broken_newline_pattern, js_content)
assert not matches, f"Found unescaped newlines in string concatenation: {matches}"
class TestEditModeIntegration:
"""Integration tests for the complete edit mode functionality."""
def test_md_render_edit_command_execution(self):
"""Test that the md-render --edit command executes without errors."""
import tempfile
from markitect.plugins.builtin.markdown_commands import md_render_command
from click.testing import CliRunner
runner = CliRunner()
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
md_file.write("# Test Document\n\nThis is a test paragraph.\n\n## Section 2\n\nAnother paragraph.")
md_file_path = md_file.name
with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as html_file:
html_file_path = html_file.name
try:
# Test the command
result = runner.invoke(md_render_command, [
md_file_path,
'--edit',
'--output', html_file_path
])
assert result.exit_code == 0, f"Command failed: {result.output}"
# Verify the output file exists and contains edit mode elements
html_content = Path(html_file_path).read_text()
assert 'MarkitectEditor' in html_content
assert 'markitect-edit-mode' in html_content
# Verify JavaScript syntax
js_match = re.search(r'<script>(.*?)</script>', html_content, re.DOTALL)
assert js_match, "No JavaScript found in output"
finally:
Path(md_file_path).unlink(missing_ok=True)
Path(html_file_path).unlink(missing_ok=True)
def test_save_functionality_javascript_presence(self):
"""Test that the save functionality JavaScript is properly included."""
from markitect.document_manager import DocumentManager
doc_manager = DocumentManager()
html_content = doc_manager._generate_html_template(
title="Test",
markdown_content="# Test Content",
edit_mode=True
)
# Check for save-related functionality
save_elements = [
'Save & Download', # Button text
'markitectEditor.save()', # Save function call
'getMarkdownContent', # Content extraction
'Blob', # File creation
'download' # Download attribute
]
for element in save_elements:
assert element in html_content, f"Save functionality element missing: {element}"
if __name__ == "__main__":
pytest.main([__file__, "-v"])

283
tools/validate_js_syntax.py Executable file
View 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)