feat: implement clean JavaScript-Python separation for edit mode

- 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>
This commit is contained in:
2025-11-14 06:41:53 +01:00
parent 26c235e296
commit 55c61a7f2d
6 changed files with 1383 additions and 32 deletions

View File

@@ -0,0 +1,247 @@
"""
Test suite for the new clean architecture implementation
Tests the JSON configuration interface and separation of concerns
"""
import pytest
import tempfile
import json
from pathlib import Path
from markitect.clean_document_manager import CleanDocumentManager
class TestCleanArchitecture:
"""Test suite for clean JavaScript-Python separation"""
def setup_method(self):
"""Setup for each test"""
self.manager = CleanDocumentManager()
def test_clean_edit_mode_json_configuration(self):
"""Test that edit mode uses clean JSON configuration interface"""
test_markdown = '''# Test Document
## Section with Problematic Content
```python
script = f"""
function test() {
console.log("Hello {name}");
}
"""
```
This content has quotes that previously broke 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:
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()
# Test 1: Check for clean template usage
assert 'markitect-config' in html_content
assert 'type="application/json"' in html_content
# Test 2: Extract and validate JSON configuration
config_json = self.extract_config_json(html_content)
assert config_json is not None, "Configuration JSON not found"
config = json.loads(config_json)
# Test 3: Validate configuration structure
required_fields = ['markdownContent', 'mode', 'theme', 'originalFilename']
for field in required_fields:
assert field in config, f"Required field '{field}' missing from configuration"
# Test 4: Check that problematic content is properly escaped
assert 'script = f"""' in config['markdownContent'] # Should be in JSON
assert '"""' not in html_content.split('markitect-config')[1].split('</script>')[0], "Unescaped quotes in HTML"
def test_clean_architecture_no_python_js_mixing(self):
"""Test that no Python code generates JavaScript strings"""
test_markdown = "# Simple Test\n\nBasic content."
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 1: No direct JavaScript variable assignments from Python
problematic_patterns = [
'const markdownContent = "', # Old way
'const markdownContentWithDogtag = "', # Old way
'var markdownContent = "',
'let markdownContent = "'
]
for pattern in problematic_patterns:
assert pattern not in html_content, f"Found problematic pattern: {pattern}"
# Test 2: Configuration should be in JSON script tag only
config_sections = html_content.count('markitect-config')
assert config_sections >= 2, f"Expected at least 2 config references (opening and closing), found {config_sections}"
# Test 3: JavaScript files should be embedded inline (no external src attributes)
js_components = [
'config-loader',
'section-manager',
'dom-renderer'
]
for component in js_components:
# Check that the component JavaScript is embedded, not referenced externally
assert f'src="js/' not in html_content, "Found external JavaScript references - should be embedded"
# Check that components are embedded inline
assert '{js_config_loader}' not in html_content, "Template placeholder not replaced"
assert 'class MarkitectConfig' in html_content, "Config loader not embedded"
assert 'class SectionManager' in html_content, "Section manager not embedded"
def test_configuration_interface_completeness(self):
"""Test that all required data is passed through the configuration interface"""
test_markdown = "# Config Test\n\nTesting configuration completeness."
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,
editor_theme='dark',
keyboard_shortcuts=False
)
assert result['success'] is True
html_content = Path(html_file.name).read_text()
config_json = self.extract_config_json(html_content)
config = json.loads(config_json)
# Test configuration completeness
expected_config = {
'markdownContent': test_markdown,
'mode': 'edit',
'theme': 'dark',
'keyboardShortcuts': False,
'autosave': False,
'sections': True,
'base64References': {}
}
for key, expected_value in expected_config.items():
assert key in config, f"Configuration missing key: {key}"
if key == 'markdownContent':
assert config[key] == expected_value, f"Configuration {key} value mismatch"
def test_insert_mode_configuration(self):
"""Test insert mode specific configuration"""
test_markdown = "# Insert Mode 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,
insert_mode=True
)
assert result['success'] is True
html_content = Path(html_file.name).read_text()
# Check body class
assert 'class="markitect-insert-mode"' in html_content
# Check configuration
config_json = self.extract_config_json(html_content)
config = json.loads(config_json)
assert config['mode'] == 'insert'
assert 'restrictedHeadingLevels' in config
assert config['restrictedHeadingLevels'] == [1, 2, 3]
def test_static_vs_edit_mode_separation(self):
"""Test that static mode and edit mode use different templates"""
test_markdown = "# Mode Test\n\nTesting template separation."
# Test static mode
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 static_file:
static_result = self.manager.render_file(
input_file=md_file.name,
output_file=static_file.name,
edit_mode=False
)
static_content = Path(static_file.name).read_text()
# Static mode should NOT have configuration interface
assert 'markitect-config' not in static_content
assert 'application/json' not in static_content
# Test edit mode
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as edit_file:
edit_result = self.manager.render_file(
input_file=md_file.name,
output_file=edit_file.name,
edit_mode=True
)
edit_content = Path(edit_file.name).read_text()
# Edit mode should HAVE configuration interface
assert 'markitect-config' in edit_content
assert 'application/json' in edit_content
# Helper methods
def extract_config_json(self, html_content):
"""Extract JSON configuration from HTML"""
try:
# Find the config script tag
start_marker = 'id="markitect-config" type="application/json">'
end_marker = '</script>'
start_pos = html_content.find(start_marker)
if start_pos == -1:
return None
start_pos += len(start_marker)
end_pos = html_content.find(end_marker, start_pos)
if end_pos == -1:
return None
config_json = html_content[start_pos:end_pos].strip()
return config_json
except Exception as e:
print(f"Failed to extract config JSON: {e}")
return None

440
tests/test_js_sanity.py Normal file
View File

@@ -0,0 +1,440 @@
"""
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}"