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:
11
Makefile
11
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
348
tests/test_issue_144_edit_mode_regression.py
Normal file
348
tests/test_issue_144_edit_mode_regression.py
Normal 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
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