- Created JSON configuration interface eliminating JavaScript-Python code mixing - Added external script references following non-edit mode patterns - Implemented edit-mode-fixed.html template with proper fallback content - Added config-loader.js for clean data transfer via JSON - Updated main-updated.js with simplified initialization (no infinite retry loops) - Added comprehensive test suite for JavaScript syntax validation - Achieved full GUARDRAILS.md compliance with clean separation of concerns Fixes infinite retry loops and JavaScript syntax errors caused by template literal escaping issues in Python f-strings. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
440 lines
17 KiB
Python
440 lines
17 KiB
Python
"""
|
|
JavaScript Sanity Test Suite
|
|
Tests for basic JavaScript functionality, syntax validation, and initialization
|
|
"""
|
|
import pytest
|
|
import tempfile
|
|
import re
|
|
from pathlib import Path
|
|
from markitect.clean_document_manager import CleanDocumentManager
|
|
|
|
|
|
class TestJSSanity:
|
|
"""Test suite for JavaScript sanity checks"""
|
|
|
|
def setup_method(self):
|
|
"""Setup for each test"""
|
|
self.manager = CleanDocumentManager()
|
|
|
|
def test_basic_html_generation_no_edit_mode(self):
|
|
"""Test that basic HTML generation works without edit mode"""
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write("# Test Document\n\nThis is a test.")
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=False
|
|
)
|
|
|
|
assert result['success'] is True
|
|
|
|
# Read generated HTML
|
|
html_content = Path(html_file.name).read_text()
|
|
|
|
# Basic HTML structure checks
|
|
assert '<!DOCTYPE html>' in html_content
|
|
assert '<html' in html_content
|
|
assert '</html>' in html_content
|
|
assert '<body' in html_content
|
|
assert '</body>' in html_content
|
|
assert 'Test Document' in html_content
|
|
|
|
def test_edit_mode_javascript_syntax_validation(self):
|
|
"""Test that edit mode generates syntactically valid JavaScript"""
|
|
test_markdown = '''# Test Document
|
|
|
|
## Code Block Test
|
|
```python
|
|
script = f"""
|
|
function test() {
|
|
console.log("test");
|
|
}
|
|
"""
|
|
```
|
|
|
|
This contains quotes that could break JavaScript.
|
|
'''
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write(test_markdown)
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=True
|
|
)
|
|
|
|
assert result['success'] is True
|
|
|
|
# Read generated HTML
|
|
html_content = Path(html_file.name).read_text()
|
|
|
|
# Extract JavaScript content
|
|
js_content = self.extract_javascript_from_html(html_content)
|
|
|
|
# Test 1: Basic syntax validation
|
|
syntax_errors = self.check_javascript_syntax(js_content)
|
|
assert len(syntax_errors) == 0, f"JavaScript syntax errors found: {syntax_errors}"
|
|
|
|
# Test 2: Check for unescaped quotes
|
|
quote_errors = self.check_for_quote_escaping_issues(js_content)
|
|
assert len(quote_errors) == 0, f"Quote escaping issues found: {quote_errors}"
|
|
|
|
# Test 3: Check for required constants
|
|
self.check_required_constants(js_content)
|
|
|
|
def test_edit_mode_component_loading(self):
|
|
"""Test that all required JavaScript components are loaded"""
|
|
test_markdown = "# Simple Test\n\nBasic content for component loading test."
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write(test_markdown)
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=True
|
|
)
|
|
|
|
assert result['success'] is True
|
|
html_content = Path(html_file.name).read_text()
|
|
|
|
# Check for required components
|
|
required_components = [
|
|
'js/core/debug-system.js',
|
|
'js/core/section-manager.js',
|
|
'js/components/dom-renderer.js',
|
|
'js/controls/control-base.js',
|
|
'js/main.js'
|
|
]
|
|
|
|
for component in required_components:
|
|
assert f"// === {component} ===" in html_content, f"Component {component} not loaded"
|
|
|
|
def test_edit_mode_class_definitions(self):
|
|
"""Test that required JavaScript classes are defined"""
|
|
test_markdown = "# Class Definition Test\n\nTesting class loading."
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write(test_markdown)
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=True
|
|
)
|
|
|
|
assert result['success'] is True
|
|
html_content = Path(html_file.name).read_text()
|
|
|
|
# Check for required class definitions
|
|
required_classes = [
|
|
'class Section',
|
|
'class SectionManager',
|
|
'class DOMRenderer',
|
|
'class MarkitectDebugSystem',
|
|
'const Control =',
|
|
'class StatusControl',
|
|
'class DebugControl',
|
|
'class EditControl'
|
|
]
|
|
|
|
for class_def in required_classes:
|
|
assert class_def in html_content, f"Class definition '{class_def}' not found"
|
|
|
|
def test_edit_mode_initialization_functions(self):
|
|
"""Test that required initialization functions are defined"""
|
|
test_markdown = "# Initialization Test\n\nTesting function definitions."
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write(test_markdown)
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=True
|
|
)
|
|
|
|
assert result['success'] is True
|
|
html_content = Path(html_file.name).read_text()
|
|
|
|
# Check for required function definitions
|
|
required_functions = [
|
|
'function initializeCleanEditor',
|
|
'function initializeScrollIndicators',
|
|
'function debug'
|
|
]
|
|
|
|
for func_def in required_functions:
|
|
assert func_def in html_content, f"Function definition '{func_def}' not found"
|
|
|
|
def test_edit_mode_global_exports(self):
|
|
"""Test that required globals are exported to window"""
|
|
test_markdown = "# Global Exports Test\n\nTesting window exports."
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write(test_markdown)
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=True
|
|
)
|
|
|
|
assert result['success'] is True
|
|
html_content = Path(html_file.name).read_text()
|
|
|
|
# Check for required window exports
|
|
required_exports = [
|
|
'window.MarkitectDebugSystem = new MarkitectDebugSystem',
|
|
'window.SectionManager = SectionManager',
|
|
'window.Control = Control',
|
|
'window.StatusControl = StatusControl'
|
|
]
|
|
|
|
for export in required_exports:
|
|
assert export in html_content, f"Window export '{export}' not found"
|
|
|
|
# Helper methods
|
|
|
|
def extract_javascript_from_html(self, html_content):
|
|
"""Extract JavaScript content from HTML"""
|
|
# Find all script tags and extract their content
|
|
script_pattern = r'<script[^>]*>(.*?)</script>'
|
|
scripts = re.findall(script_pattern, html_content, re.DOTALL)
|
|
return '\n'.join(scripts)
|
|
|
|
def check_javascript_syntax(self, js_content):
|
|
"""Basic JavaScript syntax validation"""
|
|
errors = []
|
|
|
|
# Check for common syntax errors
|
|
|
|
# 1. Unmatched quotes
|
|
single_quotes = js_content.count("'") - js_content.count("\\'")
|
|
double_quotes = js_content.count('"') - js_content.count('\\"')
|
|
|
|
if single_quotes % 2 != 0:
|
|
errors.append("Unmatched single quotes detected")
|
|
if double_quotes % 2 != 0:
|
|
errors.append("Unmatched double quotes detected")
|
|
|
|
# 2. Unmatched braces
|
|
open_braces = js_content.count('{')
|
|
close_braces = js_content.count('}')
|
|
if open_braces != close_braces:
|
|
errors.append(f"Unmatched braces: {open_braces} open, {close_braces} close")
|
|
|
|
# 3. Unmatched parentheses
|
|
open_parens = js_content.count('(')
|
|
close_parens = js_content.count(')')
|
|
if open_parens != close_parens:
|
|
errors.append(f"Unmatched parentheses: {open_parens} open, {close_parens} close")
|
|
|
|
# 4. Check for unterminated string literals
|
|
# Look for patterns that suggest unterminated strings
|
|
unterminated_patterns = [
|
|
r'[^\\]"[^"]*$', # Double quote not followed by closing quote at line end
|
|
r'[^\\]\'[^\']*$' # Single quote not followed by closing quote at line end
|
|
]
|
|
|
|
for pattern in unterminated_patterns:
|
|
matches = re.findall(pattern, js_content, re.MULTILINE)
|
|
if matches:
|
|
errors.append(f"Potential unterminated string literals: {len(matches)} found")
|
|
|
|
return errors
|
|
|
|
def check_for_quote_escaping_issues(self, js_content):
|
|
"""Check for common quote escaping problems"""
|
|
errors = []
|
|
|
|
# Look for problematic patterns
|
|
|
|
# 1. Triple quotes in JSON strings (common Python -> JS issue)
|
|
if '"""' in js_content and 'const markdownContent' in js_content:
|
|
errors.append("Triple quotes found in markdownContent - likely escaping issue")
|
|
|
|
# 2. Unescaped newlines in strings
|
|
problem_patterns = [
|
|
r'"[^"]*\n[^"]*"', # Newline in double-quoted string
|
|
r"'[^']*\n[^']*'" # Newline in single-quoted string
|
|
]
|
|
|
|
for pattern in problem_patterns:
|
|
matches = re.findall(pattern, js_content)
|
|
if matches:
|
|
errors.append(f"Unescaped newlines in strings: {len(matches)} found")
|
|
|
|
return errors
|
|
|
|
def check_required_constants(self, js_content):
|
|
"""Check that required constants are defined"""
|
|
required_constants = [
|
|
'const markdownContent =',
|
|
'const MARKITECT_EDIT_MODE =',
|
|
'const MARKITECT_EDITOR_CONFIG =',
|
|
'const EditState =',
|
|
'const SectionType ='
|
|
]
|
|
|
|
for constant in required_constants:
|
|
assert constant in js_content, f"Required constant '{constant}' not found"
|
|
|
|
def check_for_infinite_retry_loop(self, js_content):
|
|
"""Check for patterns that indicate infinite retry loops"""
|
|
errors = []
|
|
|
|
# Pattern 1: Retry logic that can loop infinitely
|
|
if "setTimeout(() => this.initialize(), 50)" in js_content:
|
|
# Check if there's a proper termination condition
|
|
if "maxWait" not in js_content and "startTime" not in js_content:
|
|
errors.append("Found retry setTimeout without timeout protection")
|
|
|
|
# Pattern 2: Configuration loading that retries indefinitely
|
|
retry_patterns = [
|
|
r"setTimeout\([^)]*initialize[^)]*\)", # setTimeout calling initialize
|
|
r"if\s*\(\s*!.*\.loaded\s*\)\s*{[^}]*setTimeout" # if not loaded, setTimeout
|
|
]
|
|
|
|
import re
|
|
for pattern in retry_patterns:
|
|
matches = re.findall(pattern, js_content)
|
|
if matches:
|
|
# Check if there are proper safeguards
|
|
if "maxWait" not in js_content or "timeout" not in js_content.lower():
|
|
errors.append(f"Found retry pattern without timeout protection: {pattern}")
|
|
|
|
# Pattern 3: Check for MarkitectMain.initialize calling itself recursively
|
|
if js_content.count("MarkitectMain.initialize") > 2: # Once for definition, once for call
|
|
if "this.initialized" not in js_content:
|
|
errors.append("MarkitectMain.initialize may call itself recursively without proper guard")
|
|
|
|
return errors
|
|
|
|
def check_configuration_loading_logic(self, js_content):
|
|
"""Check for proper configuration loading setup"""
|
|
errors = []
|
|
|
|
# Check 1: Configuration should be loaded via JSON element
|
|
if 'markitect-config' not in js_content:
|
|
errors.append("No markitect-config element found - configuration loading will fail")
|
|
|
|
# Check 2: Configuration loader should wait for DOM
|
|
if 'DOMContentLoaded' not in js_content and 'document.readyState' not in js_content:
|
|
errors.append("Configuration loading doesn't wait for DOM ready")
|
|
|
|
# Check 3: Should have proper error handling for missing config element
|
|
if "getElementById('markitect-config')" in js_content:
|
|
if "throw new Error" not in js_content and "console.error" not in js_content:
|
|
errors.append("No error handling for missing configuration element")
|
|
|
|
# Check 4: Check for proper retry logic with timeout
|
|
if "setTimeout" in js_content and "initialize" in js_content:
|
|
if "maxWait" not in js_content and "startTime" not in js_content:
|
|
errors.append("Retry logic present but no timeout mechanism found")
|
|
|
|
return errors
|
|
|
|
def test_comprehensive_edit_mode_validation(self):
|
|
"""Comprehensive test that validates the complete edit mode functionality"""
|
|
# Use the actual GUARDRAILS.md that was causing issues
|
|
test_markdown = '''# Development Guardrails
|
|
|
|
## JavaScript Code Principles
|
|
|
|
### 1. No Inline JavaScript in Python
|
|
**NEVER write JavaScript code directly from Python code**
|
|
|
|
❌ **Wrong:**
|
|
```python
|
|
script = f"""
|
|
function myFunction() {{
|
|
console.log("Hello {name}");
|
|
}}
|
|
"""
|
|
```
|
|
|
|
✅ **Correct:**
|
|
```python
|
|
# Load from external files only
|
|
components = [
|
|
'js/core/section-manager.js',
|
|
'js/components/debug-panel.js'
|
|
]
|
|
```
|
|
|
|
This is the content that was breaking the JavaScript generation.
|
|
'''
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write(test_markdown)
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
# This should not raise an exception
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=True
|
|
)
|
|
|
|
assert result['success'] is True
|
|
|
|
# Read and validate the generated HTML
|
|
html_content = Path(html_file.name).read_text()
|
|
js_content = self.extract_javascript_from_html(html_content)
|
|
|
|
# Comprehensive validation
|
|
syntax_errors = self.check_javascript_syntax(js_content)
|
|
quote_errors = self.check_for_quote_escaping_issues(js_content)
|
|
|
|
# If these fail, we have the exact same problem as reported
|
|
assert len(syntax_errors) == 0, f"SYNTAX ERRORS: {syntax_errors}"
|
|
assert len(quote_errors) == 0, f"QUOTE ESCAPING ERRORS: {quote_errors}"
|
|
|
|
# Verify all required components loaded
|
|
self.check_required_constants(js_content)
|
|
|
|
# CRITICAL: Test for infinite retry loop
|
|
retry_errors = self.check_for_infinite_retry_loop(js_content)
|
|
assert len(retry_errors) == 0, f"INFINITE RETRY LOOP DETECTED: {retry_errors}"
|
|
|
|
def test_configuration_loading_not_stuck_in_loop(self):
|
|
"""Test specifically for infinite configuration loading retry loops"""
|
|
test_markdown = "# Simple Test\n\nBasic content for testing configuration loading."
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as md_file:
|
|
md_file.write(test_markdown)
|
|
md_file.flush()
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as html_file:
|
|
result = self.manager.render_file(
|
|
input_file=md_file.name,
|
|
output_file=html_file.name,
|
|
edit_mode=True
|
|
)
|
|
|
|
assert result['success'] is True
|
|
html_content = Path(html_file.name).read_text()
|
|
|
|
# Test for infinite retry patterns
|
|
retry_issues = self.check_for_infinite_retry_loop(html_content)
|
|
assert len(retry_issues) == 0, f"INFINITE RETRY LOOP ISSUES: {retry_issues}"
|
|
|
|
# Test for proper configuration loading setup
|
|
config_issues = self.check_configuration_loading_logic(html_content)
|
|
assert len(config_issues) == 0, f"CONFIGURATION LOADING ISSUES: {config_issues}" |