diff --git a/capabilities/testdrive-jsui/README.md b/capabilities/testdrive-jsui/README.md index c2ef1176..212b7aab 100644 --- a/capabilities/testdrive-jsui/README.md +++ b/capabilities/testdrive-jsui/README.md @@ -1,21 +1,23 @@ -# TestDrive-JSUI Capability +# TestDrive-JSUI -A comprehensive JavaScript UI testing framework capability for MarkiTect. Provides seamless integration between Python and JavaScript testing environments, enabling safe development and testing of JavaScript UI components. +A standalone JavaScript UI rendering engine and testing framework. Originally developed for MarkiTect, TestDrive-JSUI is designed as an independent, reusable capability that can be integrated into any Python project. ## ๐ŸŽฏ **Purpose** TestDrive-JSUI is designed to: +- **๐Ÿ“ Render markdown with interactive JavaScript UI** for editing and viewing +- **๐Ÿ”Œ Work as a standalone plugin** or integrate with any Python project - **๐Ÿ”’ Protect existing JavaScript UI functionality** during refactoring and development -- **๐Ÿงช Integrate JavaScript tests** into the main Python test suite +- **๐Ÿงช Integrate JavaScript tests** into Python test suites - **๐Ÿ—๏ธ Provide a clean architecture** for JavaScript framework development - **๐Ÿ“Š Enable comprehensive testing** of JavaScript UI components -- **๐Ÿš€ Support future extensibility** for JavaScript framework evolution +- **๐Ÿš€ Support extensibility** for JavaScript framework evolution ## ๐Ÿ—๏ธ **Architecture** ``` -testdrive-jsui/ +capabilities/testdrive-jsui/ โ”œโ”€โ”€ src/testdrive_jsui/ # Python package โ”‚ โ”œโ”€โ”€ core/ # Core framework components โ”‚ โ”œโ”€โ”€ components/ # UI component helpers @@ -23,11 +25,26 @@ testdrive-jsui/ โ”‚ โ””โ”€โ”€ testing/ # Python-JS bridge โ”‚ โ”œโ”€โ”€ js_test_runner.py # JavaScript test execution โ”‚ โ””โ”€โ”€ integration.py # Pytest integration -โ”œโ”€โ”€ js/ # JavaScript source -โ”‚ โ”œโ”€โ”€ core/ # Core JS components -โ”‚ โ”œโ”€โ”€ components/ # UI components +โ”œโ”€โ”€ js/ # JavaScript source (consolidated) +โ”‚ โ”œโ”€โ”€ core/ # Core JS (debug-system, section-manager) +โ”‚ โ”œโ”€โ”€ components/ # UI components (dom-renderer, debug-panel) +โ”‚ โ”œโ”€โ”€ controls/ # Control panels (edit, debug, status, contents) +โ”‚ โ”œโ”€โ”€ plugins/ # JS plugins +โ”‚ โ”œโ”€โ”€ widgets/ # UI widgets โ”‚ โ”œโ”€โ”€ utils/ # JS utilities -โ”‚ โ””โ”€โ”€ tests/ # JavaScript tests +โ”‚ โ”œโ”€โ”€ tests/ # JavaScript tests +โ”‚ โ”œโ”€โ”€ config-loader.js # Configuration loader +โ”‚ โ”œโ”€โ”€ main.js # Main entry point +โ”‚ โ””โ”€โ”€ main-updated.js # Updated main (refactored) +โ”œโ”€โ”€ static/ # Static assets +โ”‚ โ”œโ”€โ”€ css/ # Stylesheets +โ”‚ โ”‚ โ”œโ”€โ”€ editor.css # Editor styles +โ”‚ โ”‚ โ”œโ”€โ”€ controls.css # Control panel styles +โ”‚ โ”‚ โ””โ”€โ”€ themes/ # Theme files +โ”‚ โ”œโ”€โ”€ images/ # Image assets +โ”‚ โ”‚ โ””โ”€โ”€ icons/ # UI icons +โ”‚ โ””โ”€โ”€ templates/ # HTML templates +โ”‚ โ””โ”€โ”€ index.html # Main template โ”œโ”€โ”€ tests/ # Python tests โ”œโ”€โ”€ Makefile # Capability commands โ”œโ”€โ”€ pyproject.toml # Python package config @@ -35,15 +52,40 @@ testdrive-jsui/ โ””โ”€โ”€ README.md # This file ``` +**Key Design Principles:** + +- **Single Source of Truth**: All assets in one capability directory +- **Self-Declaration**: Plugin declares its own paths (no hardcoded discovery) +- **Clean Boundaries**: JSON config interface between Python and JavaScript +- **No Code Mixing**: JavaScript stays in `.js` files, never embedded in Python +- **Plugin Independence**: Can be moved/installed anywhere + ## ๐Ÿš€ **Quick Start** ### Prerequisites - **Python 3.8+** with pip -- **Node.js 16+** with npm -- **MarkiTect main project** installed +- **Node.js 16+** with npm (optional, for JavaScript testing) -### Installation +### Standalone Installation + +TestDrive-JSUI can be used completely independently of MarkiTect: + +```bash +# Clone or copy this directory +git clone testdrive-jsui +cd testdrive-jsui + +# Install Python package in development mode +pip install -e . + +# (Optional) Install JavaScript dependencies for testing +npm install +``` + +### Integration with MarkiTect + +If using within the MarkiTect project: ```bash # Navigate to the capability directory @@ -58,6 +100,115 @@ make testdrive-jsui-status # Check environment make testdrive-jsui-test-all # Run all tests ``` +## ๐Ÿ“ฆ **Standalone Usage** + +### As a Rendering Engine Plugin + +TestDrive-JSUI implements a plugin interface that allows it to be used independently: + +```python +from pathlib import Path +from testdrive_jsui import TestDriveJSUIEngine + +# Create engine instance +engine = TestDriveJSUIEngine() + +# The engine declares its own location - no hardcoded paths! +source_dir = engine.get_plugin_source_dir() +print(f"Plugin located at: {source_dir}") + +# Get organized asset paths +asset_paths = engine.get_asset_paths() +# Returns: {'js': Path(...), 'css': Path(...), 'images': Path(...), 'templates': Path(...)} + +# Get required assets list +assets = engine.get_required_assets() +# Returns: {'js': [...], 'css': [...], 'images': [...], 'external': [...]} +``` + +### Rendering Documents + +Use the engine to render markdown content with an interactive JavaScript UI: + +```python +from testdrive_jsui import TestDriveJSUIEngine +from markitect.plugins.rendering import RenderingConfig +from pathlib import Path + +# Create engine +engine = TestDriveJSUIEngine() + +# Configure for development (serves from source) +config = RenderingConfig( + asset_base_url='.', + development_mode=True, + plugin_source_dirs={'testdrive-jsui': engine.get_plugin_source_dir()} +) + +# Render markdown content +markdown_content = """ +# My Document + +This is a test of the **TestDrive-JSUI** rendering engine. + +## Features +- Interactive editing +- Section management +- Debug controls +""" + +html = engine.render_document(markdown_content, 'edit', config) + +# Save to file +Path('output.html').write_text(html) +print("โœ… Rendered to output.html") +``` + +### Production Deployment + +For production, deploy assets to a static directory: + +```python +from testdrive_jsui import TestDriveJSUIEngine +from markitect.plugins.rendering import RenderingConfig +from pathlib import Path + +engine = TestDriveJSUIEngine() + +# Configure for production +config = RenderingConfig( + asset_base_url='/_static', + development_mode=False, + output_directory=Path('./dist') +) + +# Assets will be copied to: dist/_static/plugins/testdrive-jsui/ +html = engine.render_document(content, 'view', config) +``` + +### Plugin Independence + +TestDrive-JSUI is designed to be fully independent: + +- โœ… **Self-contained**: All assets in one directory +- โœ… **Self-declaring**: Plugin declares its own location +- โœ… **No hardcoded paths**: Works regardless of installation location +- โœ… **Clean interface**: Pure JSON configuration boundary +- โœ… **No JavaScript in Python**: All JS in separate files +- โœ… **Reusable**: Can be used in any Python project + +### Supported Modes + +```python +# Check supported rendering modes +modes = engine.get_supported_modes() +# Returns: ['edit', 'view'] + +# Validate a mode +if engine.validate_mode('edit'): + html = engine.render_document(content, 'edit', config) +``` + ## ๐Ÿงช **Testing** ### JavaScript Tests diff --git a/testdrive-jsui/static/js/config-loader.js b/capabilities/testdrive-jsui/js/config-loader.js similarity index 100% rename from testdrive-jsui/static/js/config-loader.js rename to capabilities/testdrive-jsui/js/config-loader.js diff --git a/testdrive-jsui/static/js/core/debug-system.js b/capabilities/testdrive-jsui/js/core/debug-system.js similarity index 100% rename from testdrive-jsui/static/js/core/debug-system.js rename to capabilities/testdrive-jsui/js/core/debug-system.js diff --git a/testdrive-jsui/static/js/main-updated.js b/capabilities/testdrive-jsui/js/main-updated.js similarity index 100% rename from testdrive-jsui/static/js/main-updated.js rename to capabilities/testdrive-jsui/js/main-updated.js diff --git a/testdrive-jsui/static/js/main.js b/capabilities/testdrive-jsui/js/main.js similarity index 100% rename from testdrive-jsui/static/js/main.js rename to capabilities/testdrive-jsui/js/main.js diff --git a/testdrive-jsui/static/js/plugins/document-navigator-plugin.js b/capabilities/testdrive-jsui/js/plugins/document-navigator-plugin.js similarity index 100% rename from testdrive-jsui/static/js/plugins/document-navigator-plugin.js rename to capabilities/testdrive-jsui/js/plugins/document-navigator-plugin.js diff --git a/testdrive-jsui/static/js/widgets/base/UIWidget.js b/capabilities/testdrive-jsui/js/widgets/base/UIWidget.js similarity index 100% rename from testdrive-jsui/static/js/widgets/base/UIWidget.js rename to capabilities/testdrive-jsui/js/widgets/base/UIWidget.js diff --git a/testdrive-jsui/static/js/widgets/base/Widget.js b/capabilities/testdrive-jsui/js/widgets/base/Widget.js similarity index 100% rename from testdrive-jsui/static/js/widgets/base/Widget.js rename to capabilities/testdrive-jsui/js/widgets/base/Widget.js diff --git a/testdrive-jsui/static/js/widgets/navigation/DocumentNavigator.js b/capabilities/testdrive-jsui/js/widgets/navigation/DocumentNavigator.js similarity index 100% rename from testdrive-jsui/static/js/widgets/navigation/DocumentNavigator.js rename to capabilities/testdrive-jsui/js/widgets/navigation/DocumentNavigator.js diff --git a/testdrive-jsui/static/css/controls.css b/capabilities/testdrive-jsui/static/css/controls.css similarity index 100% rename from testdrive-jsui/static/css/controls.css rename to capabilities/testdrive-jsui/static/css/controls.css diff --git a/testdrive-jsui/static/css/editor.css b/capabilities/testdrive-jsui/static/css/editor.css similarity index 100% rename from testdrive-jsui/static/css/editor.css rename to capabilities/testdrive-jsui/static/css/editor.css diff --git a/testdrive-jsui/static/css/themes/github.css b/capabilities/testdrive-jsui/static/css/themes/github.css similarity index 100% rename from testdrive-jsui/static/css/themes/github.css rename to capabilities/testdrive-jsui/static/css/themes/github.css diff --git a/testdrive-jsui/images/icons/edit.png b/capabilities/testdrive-jsui/static/images/icons/edit.png similarity index 100% rename from testdrive-jsui/images/icons/edit.png rename to capabilities/testdrive-jsui/static/images/icons/edit.png diff --git a/testdrive-jsui/images/icons/reset.png b/capabilities/testdrive-jsui/static/images/icons/reset.png similarity index 100% rename from testdrive-jsui/images/icons/reset.png rename to capabilities/testdrive-jsui/static/images/icons/reset.png diff --git a/testdrive-jsui/images/icons/save.png b/capabilities/testdrive-jsui/static/images/icons/save.png similarity index 100% rename from testdrive-jsui/images/icons/save.png rename to capabilities/testdrive-jsui/static/images/icons/save.png diff --git a/testdrive-jsui/templates/index.html b/capabilities/testdrive-jsui/static/templates/index.html similarity index 100% rename from testdrive-jsui/templates/index.html rename to capabilities/testdrive-jsui/static/templates/index.html diff --git a/testdrive-jsui/test-documents/sample.md b/capabilities/testdrive-jsui/test-documents/sample.md similarity index 100% rename from testdrive-jsui/test-documents/sample.md rename to capabilities/testdrive-jsui/test-documents/sample.md diff --git a/testdrive-jsui/test.html b/capabilities/testdrive-jsui/test.html similarity index 100% rename from testdrive-jsui/test.html rename to capabilities/testdrive-jsui/test.html diff --git a/markitect/plugins/rendering.py b/markitect/plugins/rendering.py index 2864683d..5f51cb8f 100644 --- a/markitect/plugins/rendering.py +++ b/markitect/plugins/rendering.py @@ -296,17 +296,21 @@ class RenderingEngineManager: return deployed_assets def _get_plugin_source_dir(self, engine_name: str) -> Optional[Path]: - """Get the source directory for a plugin.""" - if engine_name == 'testdrive-jsui': - # Look for testdrive-jsui directory relative to current directory - candidates = [ - Path('testdrive-jsui'), - Path(__file__).parent.parent.parent / 'testdrive-jsui', - Path('.') / 'testdrive-jsui' - ] + """ + Get the source directory for a plugin. + Uses plugin self-declaration if available, no hardcoded paths. + """ + engine = self.get_engine(engine_name) + if not engine: + return None - for candidate in candidates: - if candidate.exists() and candidate.is_dir(): - return candidate + # Use plugin's self-declared source directory if available + if hasattr(engine, 'get_plugin_source_dir'): + try: + source_dir = engine.get_plugin_source_dir() + if source_dir and source_dir.exists(): + return source_dir + except Exception as e: + print(f"โš ๏ธ Error getting plugin source dir for {engine_name}: {e}") return None \ No newline at end of file diff --git a/markitect/plugins/testdrive_jsui.py b/markitect/plugins/testdrive_jsui.py index 7017418a..6bddf510 100644 --- a/markitect/plugins/testdrive_jsui.py +++ b/markitect/plugins/testdrive_jsui.py @@ -35,21 +35,45 @@ class TestDriveJSUIEngine(RenderingEnginePlugin): """Support edit and view modes.""" return ["edit", "view"] + def get_plugin_source_dir(self) -> Path: + """ + Return the source directory for this plugin. + This allows the plugin to declare its own location. + """ + # Plugin is located in capabilities/testdrive-jsui/ + return Path(__file__).parent.parent.parent / "capabilities" / "testdrive-jsui" + + def get_asset_paths(self) -> Dict[str, Path]: + """ + Return paths to asset directories relative to plugin source. + This allows flexible asset organization within the plugin. + """ + base = self.get_plugin_source_dir() + return { + 'js': base / 'js', + 'css': base / 'static' / 'css', + 'images': base / 'static' / 'images', + 'templates': base / 'static' / 'templates', + } + def get_required_assets(self) -> Dict[str, List[str]]: - """Define required JavaScript, CSS, and other assets.""" + """ + Define required JavaScript, CSS, and other assets. + All paths are relative to the plugin source directory. + """ return { "js": [ - "static/js/core/debug-system.js", - "static/js/core/section-manager.js", - "static/js/components/debug-panel.js", - "static/js/components/dom-renderer.js", - "../capabilities/testdrive-jsui/js/controls/control-base.js", - "../capabilities/testdrive-jsui/js/controls/contents-control.js", - "../capabilities/testdrive-jsui/js/controls/status-control.js", - "../capabilities/testdrive-jsui/js/controls/debug-control.js", - "../capabilities/testdrive-jsui/js/controls/edit-control.js", - "static/js/config-loader.js", - "static/js/main-updated.js" + "js/core/debug-system.js", + "js/core/section-manager.js", + "js/components/debug-panel.js", + "js/components/dom-renderer.js", + "js/controls/control-base.js", + "js/controls/contents-control.js", + "js/controls/status-control.js", + "js/controls/debug-control.js", + "js/controls/edit-control.js", + "js/config-loader.js", + "js/main-updated.js" ], "css": [ "static/css/editor.css", @@ -57,9 +81,9 @@ class TestDriveJSUIEngine(RenderingEnginePlugin): "static/css/themes/github.css" ], "images": [ - "images/icons/edit.png", - "images/icons/save.png", - "images/icons/reset.png" + "static/images/icons/edit.png", + "static/images/icons/save.png", + "static/images/icons/reset.png" ], "external": [ "https://cdn.jsdelivr.net/npm/marked/marked.min.js" @@ -68,15 +92,16 @@ class TestDriveJSUIEngine(RenderingEnginePlugin): def get_template_path(self) -> Optional[Path]: """Return path to the HTML template.""" - # Look for template in plugin directory structure - plugin_dir = Path(__file__).parent.parent.parent / "testdrive-jsui" - template_path = plugin_dir / "templates" / "index.html" + # Template is in the plugin's static/templates directory + template_path = self.get_asset_paths()['templates'] / "index.html" if template_path.exists(): return template_path - # Fallback to current template location - return Path(__file__).parent.parent / "templates" / "edit-mode-fixed.html" + raise FileNotFoundError( + f"Template not found at {template_path}. " + f"Ensure testdrive-jsui is properly installed in capabilities/" + ) def render_document(self, content: str, diff --git a/testdrive-jsui/README.md b/testdrive-jsui/README.md deleted file mode 100644 index fe261c26..00000000 --- a/testdrive-jsui/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# TestDrive JSUI Plugin - -Independent JavaScript UI plugin for Markitect markdown editing. Designed for standalone development and testing of JavaScript components without requiring the full Python Markitect environment. - -## Features - -- **Independent Development**: Work on UI components without Python setup -- **Clean Architecture**: JSON-based configuration interface -- **Modular Components**: Compass-positioned control panels -- **Real-time Editing**: Click any section to edit inline -- **Asset Management**: Proper separation of JS/CSS/image assets - -## Directory Structure - -``` -testdrive-jsui/ -โ”œโ”€โ”€ static/ -โ”‚ โ”œโ”€โ”€ js/ # JavaScript components -โ”‚ โ”‚ โ”œโ”€โ”€ core/ # Core systems (debug, sections) -โ”‚ โ”‚ โ”œโ”€โ”€ components/ # UI components (panels, controls) -โ”‚ โ”‚ โ””โ”€โ”€ controls/ # Control panels (contents, status, edit, debug) -โ”‚ โ””โ”€โ”€ css/ # Stylesheets -โ”œโ”€โ”€ images/ # Icons and images -โ”œโ”€โ”€ templates/ # HTML templates -โ”œโ”€โ”€ test-documents/ # Sample markdown files -โ””โ”€โ”€ package.json # Node.js configuration - -``` - -## Development Setup - -### Standalone Development (No Python Required) - -1. **Start Development Server**: - ```bash - cd testdrive-jsui - npm run dev - # or use Python's built-in server: - python -m http.server 8080 - ``` - -2. **Open Test Document**: - Navigate to `http://localhost:8080/test.html` to see the UI in action. - -3. **Edit JavaScript**: - - Modify files in `static/js/` - - Refresh browser to see changes - - Use browser DevTools for debugging - -### Integration with Markitect - -The plugin integrates with Markitect through the rendering engine system: - -```bash -# Use with Markitect (when integrated) -markitect md-render --engine testdrive-jsui --mode edit document.md -``` - -## JavaScript Architecture - -### Core Components - -- **`main.js`**: Application entry point and initialization -- **`config-loader.js`**: Configuration interface (Python โ†” JavaScript) -- **`section-manager.js`**: Document section management -- **`dom-renderer.js`**: DOM manipulation and rendering - -### Control Panels (Compass Positioning) - -- **Northwest**: Contents/Navigation control -- **Northeast**: Edit actions control -- **East**: Status display control -- **Southeast**: Debug information control - -### Configuration Interface - -All Python data passes through a clean JSON interface: - -```html - -``` - -## Testing - -### Manual Testing -1. Load `test.html` in browser -2. Verify all controls load and position correctly -3. Test editing functionality -4. Check browser console for errors - -### Automated Testing (Future) -- Unit tests for JavaScript components -- Integration tests for HTML rendering -- Browser automation tests - -## Asset Management - -### Development Mode -- Assets served directly from `static/` directory -- Hot reloading with development server -- No build process required - -### Production Mode -- Assets copied to `_markitect/plugins/testdrive-jsui/` -- Integrated with Markitect deployment -- Configurable asset URLs - -## Configuration - -The plugin supports various configuration options: - -```json -{ - "pluginName": "testdrive-jsui", - "assetBaseUrl": "_markitect", - "developmentMode": true, - "pluginAssetDir": "_markitect/plugins/testdrive-jsui" -} -``` - -## Extending the Plugin - -### Adding New Controls -1. Create new control in `../capabilities/testdrive-jsui/js/controls/` -2. Extend `ControlBase` class -3. Register in `main.js` initialization -4. Add compass position (nw, ne, e, se, s, sw, w, nw) - -### Adding New Themes -1. Create CSS file in `static/css/themes/` -2. Update theme selection logic -3. Test with different markdown content - -### Adding New Assets -1. Add files to appropriate `static/` subdirectory -2. Update `get_required_assets()` in plugin class -3. Reference in templates or JavaScript - -## Integration Points - -The plugin interfaces with Markitect through: - -1. **Plugin Registry**: Auto-discovery of rendering engines -2. **Asset Management**: Deployment and URL generation -3. **Configuration**: JSON-based data transfer -4. **Templates**: HTML template system - -## License - -MIT License - see LICENSE file for details. \ No newline at end of file diff --git a/testdrive-jsui/package.json b/testdrive-jsui/package.json deleted file mode 100644 index 9f6de414..00000000 --- a/testdrive-jsui/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "testdrive-jsui", - "version": "1.0.0", - "description": "Independent JavaScript UI plugin for Markitect markdown editing", - "main": "static/js/main.js", - "scripts": { - "dev": "python -m http.server 8080", - "test": "echo \"No tests yet\" && exit 0", - "build": "echo \"No build process yet\" && exit 0", - "lint": "echo \"No linting yet\" && exit 0" - }, - "keywords": [ - "markitect", - "markdown", - "editor", - "javascript", - "ui" - ], - "author": "Markitect Team", - "license": "MIT", - "devDependencies": { - "http-server": "^14.1.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/markitect/testdrive-jsui.git" - }, - "files": [ - "static/", - "templates/", - "images/", - "README.md" - ] -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/components/debug-panel.js b/testdrive-jsui/static/js/components/debug-panel.js deleted file mode 100644 index d22706a0..00000000 --- a/testdrive-jsui/static/js/components/debug-panel.js +++ /dev/null @@ -1,191 +0,0 @@ -/** - * DebugPanel Component - * - * Extracted from monolithic editor.js as part of architecture refactoring. - * Handles debug message display and management for client-side debugging. - * - * Dependencies: - * - None (standalone component) - */ - -/** - * DebugPanel - Manages debug message display and interaction - */ -class DebugPanel { - constructor() { - this.messages = []; - this.isActive = false; - this.maxMessages = 1000; // Keep last 1000 messages - } - - /** - * Add a debug message - */ - addMessage(message, category = 'INFO') { - const messageObj = { - message, - category, - timestamp: new Date().toLocaleTimeString() - }; - - this.messages.push(messageObj); - - // Keep only last maxMessages - if (this.messages.length > this.maxMessages) { - this.messages = this.messages.slice(-this.maxMessages); - } - - // Auto-update if panel is visible - if (this.isActive) { - this.update(); - } - } - - /** - * Toggle the debug panel on/off - */ - toggle() { - const debugContainer = document.getElementById('debug-messages-container'); - const debugButton = document.getElementById('toggle-debug'); - - if (!debugContainer || !debugButton) { - console.warn('DebugPanel: Required DOM elements not found'); - return; - } - - if (this.isActive) { - this.hide(); - } else { - this.show(); - } - } - - /** - * Show the debug panel - */ - show() { - const debugContainer = document.getElementById('debug-messages-container'); - const debugButton = document.getElementById('toggle-debug'); - - if (!debugContainer || !debugButton) { - console.warn('DebugPanel: Required DOM elements not found'); - return; - } - - debugContainer.style.display = 'block'; - debugButton.textContent = '๐Ÿ” Debug (ON)'; - debugButton.style.background = '#28a745'; - this.isActive = true; - this.update(); - } - - /** - * Hide the debug panel - */ - hide() { - const debugContainer = document.getElementById('debug-messages-container'); - const debugButton = document.getElementById('toggle-debug'); - - if (!debugContainer || !debugButton) { - console.warn('DebugPanel: Required DOM elements not found'); - return; - } - - debugContainer.style.display = 'none'; - debugButton.textContent = '๐Ÿ” Debug'; - debugButton.style.background = '#6c757d'; - this.isActive = false; - } - - /** - * Update the debug panel with current messages - */ - update() { - const debugContainer = document.getElementById('debug-messages-container'); - if (!debugContainer || !this.isActive) { - return; - } - - if (this.messages.length === 0) { - debugContainer.innerHTML = '
No debug messages yet. Click sections to generate debug output.
'; - return; - } - - // Show the last 50 messages in reverse order (newest first) - const recentMessages = this.messages.slice(-50).reverse(); - - const messagesHtml = recentMessages.map(msg => { - const categoryColor = { - 'INFO': '#17a2b8', - 'WARNING': '#ffc107', - 'ERROR': '#dc3545', - 'SUCCESS': '#28a745', - 'DEBUG': '#6f42c1' - }[msg.category] || '#6c757d'; - - return ` -
- [${msg.timestamp}] - ${msg.category}: - ${msg.message} -
- `; - }).join(''); - - debugContainer.innerHTML = ` -
- Debug Messages (${this.messages.length} total, showing last ${recentMessages.length}) - -
-
- ${messagesHtml} -
- `; - - // Add event listener for clear button - const clearBtn = debugContainer.querySelector('#debug-clear-btn'); - if (clearBtn) { - clearBtn.addEventListener('click', () => { - this.clear(); - }); - } - - // Auto-scroll to bottom to show newest messages - const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]'); - if (scrollContainer) { - scrollContainer.scrollTop = scrollContainer.scrollHeight; - } - } - - /** - * Clear all debug messages - */ - clear() { - this.messages = []; - this.update(); - } - - /** - * Get the number of messages - */ - getMessageCount() { - return this.messages.length; - } - - /** - * Get recent messages - */ - getRecentMessages(count = 10) { - return this.messages.slice(-count); - } -} - -// Export for use in tests and other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { DebugPanel }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.DebugPanel = DebugPanel; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/components/dom-renderer.js b/testdrive-jsui/static/js/components/dom-renderer.js deleted file mode 100644 index 20748483..00000000 --- a/testdrive-jsui/static/js/components/dom-renderer.js +++ /dev/null @@ -1,1128 +0,0 @@ -/** - * DOMRenderer Component - * - * Extracted from monolithic editor.js as part of architecture refactoring. - * Handles all DOM interactions and UI rendering for section editing. - * - * Dependencies: - * - FloatingMenu component (to be extracted) - * - debug function (imported from utils) - */ - -// Import dependencies (placeholders for now) -function debug(message, category = 'INFO') { - console.log(`DEBUG ${category}: ${message}`); -} - -/** - * Simple FloatingMenu implementation (will be extracted to separate component later) - */ -class FloatingMenu { - constructor(sectionId, type, renderer) { - this.sectionId = sectionId; - this.type = type; - this.renderer = renderer; - this.element = null; - this.isVisible = false; - } - - show(contentElement, controlsElement) { - if (this.isVisible) this.hide(); - - const targetElement = this.renderer.findSectionElement(this.sectionId); - if (!targetElement) return null; - - // Get content dimensions and position - const rect = targetElement.getBoundingClientRect(); - const viewport = { - width: window.innerWidth, - height: window.innerHeight - }; - - // Calculate content width and responsive extension - const contentWidth = rect.width; - const buttonAreaWidth = 120; // Space needed for buttons - const minMenuWidth = Math.max(300, contentWidth); // At least content width or 300px - const preferredMenuWidth = contentWidth + buttonAreaWidth; - - // Check if we have space to extend to the right - const spaceOnRight = viewport.width - rect.right; - const canExtendRight = spaceOnRight >= buttonAreaWidth + 20; // 20px margin - - // Determine final menu width - let menuWidth; - if (canExtendRight && viewport.width >= 800) { // Only on wide screens - menuWidth = Math.min(preferredMenuWidth, viewport.width - rect.left - 20); - } else { - menuWidth = Math.min(minMenuWidth, viewport.width - 40); // 20px margins - } - - // Create floating menu element - this.element = document.createElement('div'); - this.element.className = 'ui-edit-floating-menu'; - this.element.style.cssText = ` - position: fixed; - z-index: 10000; - background: white; - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - padding: 0; - width: ${menuWidth}px; - box-sizing: border-box; - `; - - // Add headline - const headline = document.createElement('div'); - headline.className = 'ui-edit-headline'; - headline.textContent = `Editing ${this.type === 'image' ? 'Image' : 'Section'}`; - headline.style.cssText = ` - background: #f8f9fa; - border-bottom: 1px solid #ddd; - padding: 8px 16px; - font-weight: 600; - font-size: 12px; - color: #495057; - border-radius: 8px 8px 0 0; - text-transform: uppercase; - letter-spacing: 0.5px; - `; - - // Create content wrapper with padding - const contentWrapper = document.createElement('div'); - contentWrapper.style.cssText = ` - padding: 16px; - `; - - this.element.appendChild(headline); - - // Position directly over content (overlay positioning) - let left = rect.left; - let top = rect.top; - - // Ensure menu doesn't go off-screen horizontally - if (left + menuWidth > viewport.width) { - left = viewport.width - menuWidth - 20; - } - if (left < 10) { - left = 10; - } - - // For vertical positioning, prefer staying on top of content - // Only move if absolutely necessary - const menuHeight = this.type === 'image' ? 350 : 200; // Better height estimates - const wouldGoOffBottom = top + menuHeight > viewport.height; - const wouldGoOffTop = top < 10; - - if (wouldGoOffBottom && !wouldGoOffTop) { - // Try to fit by moving up, but keep some overlay if possible - const maxTop = viewport.height - menuHeight - 10; - top = Math.max(rect.top - 50, maxTop); // Prefer staying near original position - } else if (wouldGoOffTop) { - top = 10; // Minimum distance from top - } - // Otherwise, keep the original overlay position - - this.element.style.left = `${left}px`; - this.element.style.top = `${top}px`; - - // Add content to wrapper - if (contentElement) { - contentWrapper.appendChild(contentElement); - } - if (controlsElement) { - contentWrapper.appendChild(controlsElement); - } - - this.element.appendChild(contentWrapper); - - // Add close button to headline - const closeButton = document.createElement('button'); - closeButton.textContent = 'ร—'; - closeButton.style.cssText = ` - position: absolute; - top: 4px; - right: 8px; - background: none; - border: none; - font-size: 18px; - cursor: pointer; - color: #666; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: background-color 0.2s ease; - `; - closeButton.addEventListener('mouseover', () => { - closeButton.style.backgroundColor = '#e9ecef'; - }); - closeButton.addEventListener('mouseout', () => { - closeButton.style.backgroundColor = 'transparent'; - }); - closeButton.addEventListener('click', (event) => { - event.stopPropagation(); - this.hide(); - }); - this.element.appendChild(closeButton); - - document.body.appendChild(this.element); - this.isVisible = true; - - return this.element; - } - - hide() { - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - this.element = null; - this.isVisible = false; - - // Stop editing state in the section manager - const section = this.renderer.sectionManager.sections.get(this.sectionId); - if (section && section.isEditing()) { - section.stopEditing(); - } - - // Remove from editing sections - this.renderer.editingSections.delete(this.sectionId); - } -} - -/** - * DOMRenderer - Handles DOM interactions and section rendering - */ -class DOMRenderer { - constructor(sectionManager, container) { - this.sectionManager = sectionManager; - this.container = container; - this.editingSections = new Set(); - this.currentFloatingMenu = null; - this.eventListenersAttached = false; - this.lastClickTime = 0; - this.clickDebounceMs = 300; // Prevent rapid clicks - - // Enhanced Event System - Track event types - this.eventHistory = []; - this.eventStats = { - 'section-click': 0, - 'section-hover-enter': 0, - 'section-hover-leave': 0, - 'keyboard-shortcut': 0, - 'section-drag-start': 0, - 'section-drag-over': 0, - 'section-drop': 0, - 'section-focus-in': 0, - 'section-focus-out': 0, - 'section-context-menu': 0 - }; - - // Bind event handlers - this.handleSectionClick = this.handleSectionClick.bind(this); - this.handleKeydown = this.handleKeydown.bind(this); - - this.setupEventListeners(); - } - - setupEventListeners() { - this.sectionManager.on('sections-created', (data) => { - this.renderAllSections(data.sections); - }); - this.sectionManager.on('edit-started', (data) => { - debug('EVENT: edit-started event received for: ' + data.sectionId, 'EVENT'); - this.showEditor(data.sectionId, data.content); - }); - } - - /** - * Render all sections to the DOM - */ - renderAllSections(sections) { - debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER'); - - // Clear container - this.container.innerHTML = ''; - debug('22: Container cleared', 'RENDER'); - - const contentArea = this.container.querySelector('#markdown-content') || this.container; - - // Render each section - sections.forEach((section, index) => { - debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER'); - const element = this.renderSection(section); - if (element) { - contentArea.appendChild(element); - } - }); - - debug('24: All section elements added to container', 'RENDER'); - - // Attach event listeners only once - if (!this.eventListenersAttached) { - this.container.addEventListener('click', this.handleSectionClick); - this.eventListenersAttached = true; - debug('25: Enhanced event listeners attached for the first time', 'RENDER'); - } else { - debug('25: Event listeners already attached, skipping', 'RENDER'); - } - - debug('25: Container content length: ' + this.container.innerHTML.length, 'RENDER'); - } - - /** - * Render a single section to DOM element - */ - renderSection(section) { - const element = document.createElement('div'); - element.className = 'ui-edit-section'; - element.setAttribute('data-section-id', section.id); - - // Add section content - // Render all sections using markdown rendering (images need HTML conversion too) - const content = this.simpleMarkdownRender(section.currentMarkdown); - element.innerHTML = content; - - // Add styling - element.style.cssText = ` - margin: 16px 0; - padding: 12px; - border: 1px solid transparent; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - `; - - element.addEventListener('mouseenter', () => { - element.style.backgroundColor = 'rgba(0, 122, 204, 0.05)'; - element.style.borderColor = 'rgba(0, 122, 204, 0.2)'; - }); - - element.addEventListener('mouseleave', () => { - if (!section.isEditing()) { - element.style.backgroundColor = 'transparent'; - element.style.borderColor = 'transparent'; - } - }); - - debug('SECTION: Section element setup complete for ' + section.id, 'SECTION'); - return element; - } - - /** - * Simple markdown rendering (placeholder) - */ - simpleMarkdownRender(markdown) { - return markdown - .replace(/^# (.*$)/gim, '

$1

') - .replace(/^## (.*$)/gim, '

$1

') - .replace(/^### (.*$)/gim, '

$1

') - .replace(/!\[(.*?)\]\((.*?)\)/gim, '$1') - .replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '$1') - .replace(/\*\*(.*?)\*\*/gim, '$1') - .replace(/\*(.*?)\*/gim, '$1') - .replace(/\n/gim, '
'); - } - - /** - * Find DOM element for a section - */ - findSectionElement(sectionId) { - return this.container.querySelector(`[data-section-id="${sectionId}"]`); - } - - /** - * Handle section click events - */ - handleSectionClick(event) { - debug('handleSectionClick: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK'); - - // Debounce rapid clicks - const now = Date.now(); - if (now - this.lastClickTime < this.clickDebounceMs) { - debug('handleSectionClick: Click debounced (too rapid)', 'CLICK'); - return; - } - this.lastClickTime = now; - - // Don't handle clicks on form elements, buttons, or links - if (event.target.closest('textarea, button, input, a')) { - debug('handleSectionClick: Ignoring click on form element', 'CLICK'); - return; - } - - const sectionElement = event.target.closest('.ui-edit-section'); - debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK'); - if (!sectionElement) return; - - const sectionId = sectionElement.getAttribute('data-section-id'); - debug('handleSectionClick: Section ID: ' + sectionId, 'CLICK'); - if (!sectionId) return; - - // Track the click event - this.trackEvent('section-click', { - sectionId, - event, - timestamp: Date.now() - }); - - // Check if this section is already being edited - const section = this.sectionManager.sections.get(sectionId); - debug('handleSectionClick: Found section object: ' + (section ? 'YES' : 'NO'), 'CLICK'); - - if (section && section.isEditing()) { - debug('handleSectionClick: Section already being edited, checking if dialog is visible: ' + sectionId, 'CLICK'); - // If section is editing but no dialog is visible, allow re-opening - const existingDialog = document.querySelector('.ui-edit-floating-menu'); - if (existingDialog) { - debug('handleSectionClick: Dialog already visible, ignoring click', 'CLICK'); - return; - } else { - debug('handleSectionClick: Section editing but no dialog visible, proceeding to show editor', 'CLICK'); - } - } - - debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK'); - - try { - debug('handleSectionClick: Calling sectionManager.startEditing', 'CLICK'); - this.sectionManager.startEditing(sectionId); - debug('handleSectionClick: Successfully called startEditing', 'CLICK'); - } catch (error) { - debug('handleSectionClick: ERROR in startEditing: ' + error.message, 'ERROR'); - console.error('Failed to start editing:', error); - } - } - - /** - * Show editor for a section - */ - showEditor(sectionId, content) { - debug('showEditor: called for section: ' + sectionId, 'EDITOR'); - - const element = this.findSectionElement(sectionId); - debug('showEditor: Found element: ' + (element ? 'YES' : 'NO'), 'EDITOR'); - if (!element) return; - - debug('showEditor: About to hide current editor', 'EDITOR'); - this.hideCurrentEditor(); - debug('showEditor: Hidden current editor', 'EDITOR'); - - const section = this.sectionManager.sections.get(sectionId); - const isImageSection = section && section.isImage(); - - if (isImageSection) { - this.showImageEditor(sectionId, section); - return; - } - - // Create content area for text editing - const editorContent = document.createElement('div'); - editorContent.className = 'ui-edit-editor-content'; - - // Check if we have space for side-by-side layout - const targetElement = this.findSectionElement(sectionId); - const rect = targetElement ? targetElement.getBoundingClientRect() : null; - const viewport = { width: window.innerWidth, height: window.innerHeight }; - const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120; - - if (hasWideLayout) { - // Side-by-side layout: textarea on left, controls on right - editorContent.style.cssText = ` - display: flex; - gap: 16px; - flex: 1; - min-width: 0; - align-items: flex-start; - `; - } else { - // Stacked layout: textarea above, controls below - editorContent.style.cssText = ` - display: flex; - flex-direction: column; - gap: 12px; - flex: 1; - min-width: 0; - `; - } - - // Create textarea container - const textareaContainer = document.createElement('div'); - textareaContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;'; - - // Create textarea - const textarea = document.createElement('textarea'); - textarea.value = content || section.currentMarkdown; - textarea.style.cssText = ` - width: 100%; - min-height: 120px; - padding: 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - line-height: 1.5; - resize: vertical; - box-sizing: border-box; - `; - - // Create controls - const controls = document.createElement('div'); - if (hasWideLayout) { - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - min-width: 100px; - flex-shrink: 0; - `; - } else { - controls.style.cssText = ` - display: flex; - gap: 8px; - justify-content: flex-end; - flex-wrap: wrap; - `; - } - - const acceptButton = document.createElement('button'); - acceptButton.textContent = hasWideLayout ? 'โœ“' : 'Accept'; - acceptButton.style.cssText = ` - background: #28a745; - color: white; - border: none; - padding: ${hasWideLayout ? '8px 12px' : '8px 16px'}; - border-radius: 4px; - cursor: pointer; - ${hasWideLayout ? 'width: 100%;' : ''} - font-size: ${hasWideLayout ? '14px' : '13px'}; - `; - - const cancelButton = document.createElement('button'); - cancelButton.textContent = hasWideLayout ? 'โœ—' : 'Cancel'; - cancelButton.style.cssText = ` - background: #dc3545; - color: white; - border: none; - padding: ${hasWideLayout ? '8px 12px' : '8px 16px'}; - border-radius: 4px; - cursor: pointer; - ${hasWideLayout ? 'width: 100%;' : ''} - font-size: ${hasWideLayout ? '14px' : '13px'}; - `; - - const resetButton = document.createElement('button'); - resetButton.textContent = hasWideLayout ? 'โ†บ' : 'โ†บ Reset'; - resetButton.style.cssText = ` - background: #fd7e14; - color: white; - border: none; - padding: ${hasWideLayout ? '8px 12px' : '8px 16px'}; - border-radius: 4px; - cursor: pointer; - ${hasWideLayout ? 'width: 100%;' : ''} - font-size: ${hasWideLayout ? '14px' : '13px'}; - `; - - controls.appendChild(acceptButton); - controls.appendChild(cancelButton); - controls.appendChild(resetButton); - - // Assemble the layout - textareaContainer.appendChild(textarea); - - if (hasWideLayout) { - editorContent.appendChild(textareaContainer); - editorContent.appendChild(controls); - } else { - editorContent.appendChild(textareaContainer); - editorContent.appendChild(controls); - } - - // Create floating menu - const floatingMenu = new FloatingMenu(sectionId, 'text', this); - this.currentFloatingMenu = floatingMenu; - this.editingSections.add(sectionId); - - floatingMenu.show(editorContent); - - // Add event listeners - acceptButton.addEventListener('click', () => { - this.sectionManager.updateContent(sectionId, textarea.value); - this.sectionManager.acceptChanges(sectionId); - floatingMenu.hide(); - this.currentFloatingMenu = null; // Clear reference - }); - - cancelButton.addEventListener('click', () => { - this.sectionManager.cancelChanges(sectionId); - floatingMenu.hide(); - this.currentFloatingMenu = null; // Clear reference - }); - - resetButton.addEventListener('click', () => { - // Reset textarea to original content and apply the change - const section = this.sectionManager.sections.get(sectionId); - if (section) { - textarea.value = section.originalMarkdown; - // Actually update the section content to original and accept the changes - this.sectionManager.updateContent(sectionId, section.originalMarkdown); - this.sectionManager.acceptChanges(sectionId); - // Close the editor - floatingMenu.hide(); - this.currentFloatingMenu = null; - } - }); - - // Auto-focus textarea - setTimeout(() => textarea.focus(), 100); - } - - /** - * Show advanced image editor with drag & drop, file upload, and preview - */ - showImageEditor(sectionId, section) { - debug('showImageEditor: called for image section: ' + sectionId, 'EDITOR'); - - // Track staging state for this editor - const stagingState = { - originalMarkdown: section.originalMarkdown, - currentAltText: '', - currentImageSrc: '', - stagedImageSrc: null, - stagedAltText: null, - hasChanges: false - }; - - // Parse markdown to extract image info - const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/); - if (imageMatch) { - const [, altText, imageSrc] = imageMatch; - stagingState.currentAltText = altText; - stagingState.currentImageSrc = imageSrc; - } - - // Check if we have space for side-by-side layout - const targetElement = this.findSectionElement(sectionId); - const rect = targetElement ? targetElement.getBoundingClientRect() : null; - const viewport = { width: window.innerWidth, height: window.innerHeight }; - const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120; - - // Create image editor content area - const editorContent = document.createElement('div'); - editorContent.className = 'ui-edit-image-content'; - - if (hasWideLayout) { - // Side-by-side layout: content on left, controls on right - editorContent.style.cssText = ` - display: flex; - gap: 16px; - flex: 1; - min-width: 0; - align-items: flex-start; - `; - } else { - // Stacked layout: content above, controls below - editorContent.style.cssText = ` - display: flex; - flex-direction: column; - gap: 15px; - flex: 1; - min-width: 0; - `; - } - - // Create content container for image and alt text - const contentContainer = document.createElement('div'); - contentContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;'; - if (!hasWideLayout) { - contentContainer.style.cssText += ` - display: flex; - flex-direction: column; - gap: 15px; - `; - } else { - contentContainer.style.cssText += ` - display: flex; - flex-direction: column; - gap: 12px; - `; - } - - // Image preview with drop zone - const imagePreview = document.createElement('div'); - imagePreview.className = 'ui-edit-image-preview'; - imagePreview.style.cssText = ` - width: 100%; - height: 180px; - text-align: center; - background: white; - padding: 12px; - border-radius: 8px; - border: 2px dashed #007bff; - transition: all 0.3s ease; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: relative; - box-sizing: border-box; - overflow: hidden; - `; - - // Function to update image preview - const updateImagePreview = (imageSrc, altText) => { - imagePreview.innerHTML = ''; - - if (imageSrc) { - const img = document.createElement('img'); - img.src = imageSrc; - img.alt = altText || ''; - img.style.cssText = ` - max-width: 100%; - max-height: 150px; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - `; - imagePreview.appendChild(img); - - // Add overlay for drop zone - const overlay = document.createElement('div'); - overlay.className = 'drop-overlay'; - overlay.style.cssText = ` - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 123, 255, 0.1); - border-radius: 6px; - display: none; - align-items: center; - justify-content: center; - color: #007bff; - font-weight: bold; - font-size: 16px; - `; - overlay.textContent = '๐Ÿ“ Drop new image here'; - imagePreview.appendChild(overlay); - } else { - // Show drop zone placeholder - const placeholder = document.createElement('div'); - placeholder.style.cssText = ` - text-align: center; - color: #6c757d; - font-size: 14px; - `; - placeholder.innerHTML = ` -
๐Ÿ“
-
Drop image here or click to select
-
Supports JPG, PNG, GIF, WebP
- `; - imagePreview.appendChild(placeholder); - } - }; - - // Initialize preview - updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText); - - // File input for image selection - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/*'; - fileInput.style.display = 'none'; - - // Function to handle image file selection - const handleImageFile = (file) => { - if (file && file.type.startsWith('image/')) { - const reader = new FileReader(); - reader.onload = (event) => { - stagingState.stagedImageSrc = event.target.result; - stagingState.hasChanges = true; - updateImagePreview(stagingState.stagedImageSrc, altTextInput.value); - updateChangeIndicator(); - }; - reader.readAsDataURL(file); - } - }; - - // Drag and drop functionality - imagePreview.addEventListener('dragover', (e) => { - e.preventDefault(); - imagePreview.style.borderColor = '#28a745'; - imagePreview.style.backgroundColor = '#f8fff8'; - const overlay = imagePreview.querySelector('.drop-overlay'); - if (overlay) overlay.style.display = 'flex'; - }); - - imagePreview.addEventListener('dragleave', (e) => { - e.preventDefault(); - imagePreview.style.borderColor = '#007bff'; - imagePreview.style.backgroundColor = 'white'; - const overlay = imagePreview.querySelector('.drop-overlay'); - if (overlay) overlay.style.display = 'none'; - }); - - imagePreview.addEventListener('drop', (e) => { - e.preventDefault(); - imagePreview.style.borderColor = '#007bff'; - imagePreview.style.backgroundColor = 'white'; - const overlay = imagePreview.querySelector('.drop-overlay'); - if (overlay) overlay.style.display = 'none'; - - const files = e.dataTransfer.files; - if (files.length > 0) { - handleImageFile(files[0]); - } - }); - - // Click to select file - imagePreview.addEventListener('click', () => { - fileInput.click(); - }); - - fileInput.addEventListener('change', (e) => { - if (e.target.files.length > 0) { - handleImageFile(e.target.files[0]); - } - }); - - // Alt text editor - const altTextContainer = document.createElement('div'); - altTextContainer.className = 'ui-edit-alt-text-container'; - altTextContainer.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - `; - - const altTextLabel = document.createElement('label'); - altTextLabel.textContent = 'Alt Text Description:'; - altTextLabel.style.cssText = ` - font-size: 13px; - font-weight: 600; - color: #333; - margin: 0; - `; - - const altTextInput = document.createElement('input'); - altTextInput.type = 'text'; - altTextInput.value = stagingState.currentAltText; - altTextInput.style.cssText = ` - width: 100%; - padding: 10px 12px; - border: 1px solid #ddd; - border-radius: 6px; - font-size: 14px; - box-sizing: border-box; - outline: none; - transition: border-color 0.2s ease; - `; - - altTextInput.addEventListener('focus', () => { - altTextInput.style.borderColor = '#007bff'; - }); - - altTextInput.addEventListener('blur', () => { - altTextInput.style.borderColor = '#ddd'; - }); - - // Track alt text changes - altTextInput.addEventListener('input', () => { - stagingState.stagedAltText = altTextInput.value; - stagingState.hasChanges = altTextInput.value !== stagingState.currentAltText || stagingState.stagedImageSrc !== null; - updateChangeIndicator(); - }); - - altTextContainer.appendChild(altTextLabel); - altTextContainer.appendChild(altTextInput); - - // Change indicator - const changeIndicator = document.createElement('div'); - changeIndicator.className = 'change-indicator'; - changeIndicator.style.cssText = ` - padding: 8px 12px; - background: #fff3cd; - border: 1px solid #ffeaa7; - border-radius: 6px; - color: #856404; - font-size: 12px; - text-align: center; - display: none; - font-weight: 500; - `; - changeIndicator.textContent = 'โš ๏ธ You have unsaved changes'; - - const updateChangeIndicator = () => { - if (stagingState.hasChanges) { - changeIndicator.style.display = 'block'; - } else { - changeIndicator.style.display = 'none'; - } - }; - - // Assemble content container - contentContainer.appendChild(imagePreview); - contentContainer.appendChild(altTextContainer); - contentContainer.appendChild(changeIndicator); - contentContainer.appendChild(fileInput); - - // Create controls - const controls = document.createElement('div'); - controls.className = 'ui-edit-controls'; - if (hasWideLayout) { - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - min-width: 100px; - flex-shrink: 0; - `; - } else { - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - `; - } - - const acceptBtn = document.createElement('button'); - acceptBtn.textContent = hasWideLayout ? 'โœ“' : 'โœ“ Accept'; - acceptBtn.style.cssText = ` - padding: ${hasWideLayout ? '8px 12px' : '8px 12px'}; - font-size: ${hasWideLayout ? '14px' : '12px'}; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - background: #28a745; - `; - - const cancelBtn = document.createElement('button'); - cancelBtn.textContent = hasWideLayout ? 'โœ—' : 'โœ— Cancel'; - cancelBtn.style.cssText = ` - padding: ${hasWideLayout ? '8px 12px' : '8px 12px'}; - font-size: ${hasWideLayout ? '14px' : '12px'}; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - background: #dc3545; - `; - - const resetBtn = document.createElement('button'); - resetBtn.textContent = hasWideLayout ? 'โ†บ' : 'โ†บ Reset'; - resetBtn.style.cssText = ` - padding: ${hasWideLayout ? '8px 12px' : '8px 12px'}; - font-size: ${hasWideLayout ? '14px' : '12px'}; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - background: #fd7e14; - `; - - controls.appendChild(acceptBtn); - controls.appendChild(cancelBtn); - controls.appendChild(resetBtn); - - - // Event handlers - acceptBtn.addEventListener('click', () => { - // Apply staged changes only when accept is clicked - if (stagingState.hasChanges) { - let newMarkdown = stagingState.originalMarkdown; - - // Apply image source change if staged - if (stagingState.stagedImageSrc !== null) { - const currentImageMatch = newMarkdown.match(/!\[(.*?)\]\((.*?)\)/); - if (currentImageMatch) { - newMarkdown = newMarkdown.replace( - /!\[(.*?)\]\((.*?)\)/, - `![${currentImageMatch[1]}](${stagingState.stagedImageSrc})` - ); - } - } - - // Apply alt text change if staged - if (stagingState.stagedAltText !== null) { - newMarkdown = newMarkdown.replace( - /!\[(.*?)\]/, - `![${stagingState.stagedAltText}]` - ); - } - - // Update section with final changes - this.sectionManager.updateContent(sectionId, newMarkdown); - } - - // Accept changes and hide editor - this.sectionManager.acceptChanges(sectionId); - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - }); - - cancelBtn.addEventListener('click', () => { - // Discard all staged changes and hide editor - this.sectionManager.cancelChanges(sectionId); - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - }); - - resetBtn.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - - // Reset to original content - const originalImageMatch = stagingState.originalMarkdown.match(/!\[(.*?)\]\((.*?)\)/); - - if (originalImageMatch) { - const [, originalAltText, originalImageSrc] = originalImageMatch; - - // Update staging state to original values - stagingState.currentAltText = originalAltText; - stagingState.currentImageSrc = originalImageSrc; - - // Clear any staged changes - stagingState.stagedImageSrc = null; - stagingState.stagedAltText = null; - stagingState.hasChanges = false; - - // Reset alt text input to original - altTextInput.value = originalAltText; - - // Trigger input event to ensure UI consistency - const inputEvent = new Event('input', { bubbles: true, cancelable: true }); - altTextInput.dispatchEvent(inputEvent); - - // Reset preview to original image - updateImagePreview(originalImageSrc, originalAltText); - - // Update change indicator - updateChangeIndicator(); - - // Actually update the section content to original and accept the changes - this.sectionManager.updateContent(sectionId, stagingState.originalMarkdown); - this.sectionManager.acceptChanges(sectionId); - - // Close the editor - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - } - }); - - // Assemble the final layout - if (hasWideLayout) { - editorContent.appendChild(contentContainer); - editorContent.appendChild(controls); - } else { - editorContent.appendChild(contentContainer); - editorContent.appendChild(controls); - } - - // Create floating menu - const floatingMenu = new FloatingMenu(sectionId, 'image', this); - this.currentFloatingMenu = floatingMenu; - this.editingSections.add(sectionId); - - floatingMenu.show(editorContent); - } - - /** - * Hide current editor - */ - hideCurrentEditor() { - debug('EDITOR: hideCurrentEditor called', 'EDITOR'); - - if (this.currentFloatingMenu) { - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - } - - debug('EDITOR: hideCurrentEditor completed', 'EDITOR'); - } - - /** - * Track event for analytics - */ - trackEvent(eventType, data) { - const eventRecord = { - type: eventType, - data: data, - timestamp: new Date().toISOString() - }; - - this.eventHistory.push(eventRecord); - if (this.eventStats.hasOwnProperty(eventType)) { - this.eventStats[eventType]++; - } - - // Keep only last 100 events - if (this.eventHistory.length > 100) { - this.eventHistory = this.eventHistory.slice(-100); - } - } - - /** - * Get event statistics - */ - getEventStats() { - const totalEvents = Object.values(this.eventStats).reduce((sum, count) => sum + count, 0); - - return { - stats: { ...this.eventStats }, - totalEvents, - recentEvents: this.eventHistory.slice(-10) - }; - } - - /** - * Handle keyboard shortcuts - */ - handleKeydown(event) { - // Basic keyboard shortcut handling - if (event.ctrlKey || event.metaKey) { - if (event.key === 'Enter') { - // Accept changes - const activeSection = Array.from(this.editingSections)[0]; - if (activeSection) { - this.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+enter', action: 'accept' }); - } - } else if (event.key === 'Escape') { - // Cancel changes - const activeSection = Array.from(this.editingSections)[0]; - if (activeSection) { - this.trackEvent('keyboard-shortcut', { shortcut: 'escape', action: 'cancel' }); - this.hideCurrentEditor(); - } - } - } - } -} - -// Export for use in tests and other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { DOMRenderer, FloatingMenu }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.DOMRenderer = DOMRenderer; - window.FloatingMenu = FloatingMenu; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/core/section-manager.js b/testdrive-jsui/static/js/core/section-manager.js deleted file mode 100644 index b1dc6fd0..00000000 --- a/testdrive-jsui/static/js/core/section-manager.js +++ /dev/null @@ -1,544 +0,0 @@ -/** - * SectionManager Component - * - * Extracted from monolithic editor.js as part of architecture refactoring. - * Manages the collection of sections and their state transitions. - * - * Dependencies: - * - EditState enum (imported) - * - SectionType enum (imported) - * - Section class (imported) - * - debug function (imported) - */ - -// Import dependencies - these will be separate modules -const EditState = Object.freeze({ - ORIGINAL: 'original', - EDITING: 'editing', - MODIFIED: 'modified', - SAVED: 'saved' -}); - -const SectionType = Object.freeze({ - HEADING: 'heading', - PARAGRAPH: 'paragraph', - LIST: 'list', - CODE: 'code', - QUOTE: 'quote', - TABLE: 'table', - HR: 'hr', - IMAGE: 'image' -}); - -// Debug function (will be extracted to utils) -function debug(message, category = 'INFO') { - // Simple console debug for now - will be enhanced later - console.log(`DEBUG ${category}: ${message}`); -} - -/** - * Section Class - manages individual section state and content - */ -class Section { - constructor(id, markdown, type) { - this.id = id; - this.originalMarkdown = markdown; - this.currentMarkdown = markdown; - this.editingMarkdown = markdown; - this.pendingMarkdown = null; - this.type = type; - this.state = EditState.ORIGINAL; - this.domElement = null; - this.lastSaved = null; - this.created = new Date(); - } - - static generateId(markdown, position, strategy = 'hash', parentId = null) { - return this.generateIdWithStrategy(markdown, position, strategy, parentId); - } - - static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) { - const sanitizedContent = this.sanitizeContentForId(markdown); - const normalizedContent = this.normalizeContentForHashing(sanitizedContent); - const sectionType = this.detectType(markdown); - - switch (strategy) { - case 'timestamp': - return this.generateTimestampId(normalizedContent, position, sectionType); - case 'sequential': - return this.generateSequentialId(normalizedContent, position, sectionType); - case 'hierarchical': - return this.generateHierarchicalId(normalizedContent, position, parentId); - case 'hash': - default: - return this.generateAdvancedId(normalizedContent, position, sectionType); - } - } - - static generateAdvancedId(content, position, sectionType) { - const contentHash = this.generateCryptoHash(content); - const safeType = sectionType || 'paragraph'; - const typePrefix = safeType.substring(0, 3); - const positionHex = position.toString(16).padStart(2, '0'); - - return `section-${typePrefix}-${contentHash}-${positionHex}`; - } - - static generateCryptoHash(content) { - let hash = 0; - if (content.length === 0) return '00000000'; - - for (let i = 0; i < content.length; i++) { - const char = content.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - - const hexHash = Math.abs(hash).toString(16).padStart(8, '0'); - return hexHash.substring(0, 8); - } - - static normalizeContentForHashing(content) { - if (!content || typeof content !== 'string') { - return ''; - } - - return content - .trim() - .replace(/\s+/g, ' ') - .replace(/\r\n/g, '\n') - .toLowerCase(); - } - - static sanitizeContentForId(content) { - if (!content || typeof content !== 'string') { - return ''; - } - - return content - .replace(/<[^>]*>/g, '') - .replace(/javascript:/gi, '') - .replace(/[^\w\s\-_.#]/g, '') - .trim(); - } - - static generateTimestampId(content, position = 0, sectionType = 'paragraph') { - const timestamp = Date.now().toString(36); - const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4); - const safeType = sectionType || 'paragraph'; - const typePrefix = safeType.substring(0, 3); - - return `section-${typePrefix}-${contentSnippet}-${timestamp}`; - } - - static generateSequentialId(content, position, sectionType = 'paragraph') { - const safeType = sectionType || 'paragraph'; - const typePrefix = safeType.substring(0, 3); - const seqNumber = (position || 0).toString().padStart(3, '0'); - const contentHash = this.generateCryptoHash(content || '').substring(0, 4); - - return `section-${typePrefix}-seq${seqNumber}-${contentHash}`; - } - - static generateHierarchicalId(content, position, parentId = null) { - const contentHash = this.generateCryptoHash(content || '').substring(0, 6); - - if (parentId) { - const childIndex = (position || 0).toString().padStart(2, '0'); - return `${parentId}-child-${childIndex}-${contentHash}`; - } else { - return `section-root-${position || 0}-${contentHash}`; - } - } - - static detectType(markdown) { - if (!markdown || typeof markdown !== 'string') { - return SectionType.PARAGRAPH; - } - - const content = markdown.replace(/^\n+|\n+$/g, ''); - if (!content) { - return SectionType.PARAGRAPH; - } - - const trimmed = content.trim(); - - // Detection order matters - most specific first - if (this.isHeading(trimmed)) { - return SectionType.HEADING; - } - - if (this.isImage(trimmed)) { - return SectionType.IMAGE; - } - - if (this.isCodeBlock(trimmed)) { - return SectionType.CODE; - } - - return SectionType.PARAGRAPH; - } - - static isHeading(trimmed) { - const headingPattern = /^#{1,6}\s+.+/; - return headingPattern.test(trimmed); - } - - static isImage(trimmed) { - const imagePattern = /!\[.*?\]\([^)]+\)/; - return imagePattern.test(trimmed); - } - - static isCodeBlock(trimmed) { - if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) { - return true; - } - if (trimmed.includes('```') || trimmed.includes('~~~')) { - const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/; - if (codeBlockPattern.test(trimmed)) { - return true; - } - } - return false; - } - - startEdit() { - if (this.state === EditState.EDITING) { - throw new Error(`Section ${this.id} is already being edited`); - } - this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown; - this.state = EditState.EDITING; - return this.editingMarkdown; - } - - updateContent(markdown) { - if (this.state !== EditState.EDITING) { - throw new Error(`Section ${this.id} is not in editing state`); - } - this.editingMarkdown = markdown; - } - - acceptChanges() { - if (this.state !== EditState.EDITING) { - throw new Error(`Section ${this.id} is not in editing state`); - } - this.currentMarkdown = this.editingMarkdown; - this.editingMarkdown = null; - this.pendingMarkdown = null; - this.state = EditState.SAVED; - this.lastSaved = new Date(); - return this.currentMarkdown; - } - - cancelChanges() { - if (this.state !== EditState.EDITING) { - throw new Error(`Section ${this.id} is not in editing state`); - } - this.editingMarkdown = null; - if (this.pendingMarkdown !== null) { - this.state = EditState.MODIFIED; - return this.pendingMarkdown; - } else if (this.lastSaved !== null) { - this.state = EditState.SAVED; - return this.currentMarkdown; - } else { - this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL; - return this.currentMarkdown; - } - } - - stopEditing() { - if (this.state !== EditState.EDITING) { - return this.state; - } - - if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) { - this.pendingMarkdown = this.editingMarkdown; - this.state = EditState.MODIFIED; - } else { - this.pendingMarkdown = null; - if (this.lastSaved !== null) { - this.state = EditState.SAVED; - } else { - this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL; - } - } - - this.editingMarkdown = null; - return this.state; - } - - resetToOriginal() { - this.currentMarkdown = this.originalMarkdown; - this.editingMarkdown = this.originalMarkdown; - this.pendingMarkdown = null; - this.state = EditState.ORIGINAL; - return this.originalMarkdown; - } - - isEditing() { - return this.state === EditState.EDITING; - } - - hasChanges() { - return this.currentMarkdown !== this.originalMarkdown; - } - - getStatus() { - return { - id: this.id, - state: this.state, - hasChanges: this.hasChanges(), - isEditing: this.isEditing(), - contentLength: this.currentMarkdown.length, - lastSaved: this.lastSaved, - type: this.type, - originalLength: this.originalMarkdown.length, - currentLength: this.currentMarkdown.length - }; - } - - isImage() { - return this.type === SectionType.IMAGE; - } - - redetectType(content = null) { - const markdown = content || this.currentMarkdown; - const oldType = this.type; - this.type = Section.detectType(markdown); - - if (oldType !== this.type) { - debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION'); - } - - return this.type; - } -} - -/** - * SectionManager - Manages the collection of sections - */ -class SectionManager { - constructor() { - this.sections = new Map(); - this.listeners = new Map(); - this.statusInterval = null; - this.lastStatusUpdate = new Date().toISOString(); - } - - on(event, callback) { - if (!this.listeners.has(event)) { - this.listeners.set(event, []); - } - this.listeners.get(event).push(callback); - } - - emit(event, data) { - if (this.listeners.has(event)) { - this.listeners.get(event).forEach(callback => callback(data)); - } - } - - createSectionsFromMarkdown(markdownContent) { - // Split content into blocks separated by double newlines - const blocks = markdownContent.split(/\n\s*\n/); - const sections = []; - let position = 0; - - for (const block of blocks) { - const trimmedBlock = block.trim(); - if (!trimmedBlock) continue; - - // Check if this block should be split further - const lines = trimmedBlock.split('\n'); - let currentSection = ''; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const isHeading = /^#{1,6}\s/.test(line.trim()); - const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line); - - // Each heading or image starts a new section - if ((isHeading || isImage) && currentSection.trim()) { - // Save the previous section - const sectionId = Section.generateId(currentSection, position); - const sectionType = Section.detectType(currentSection); - const section = new Section(sectionId, currentSection.trim(), sectionType); - sections.push(section); - this.sections.set(sectionId, section); - position++; - currentSection = line; - } else { - if (currentSection) currentSection += '\n'; - currentSection += line; - } - } - - // Save the final section from this block - if (currentSection.trim()) { - const sectionId = Section.generateId(currentSection, position); - const sectionType = Section.detectType(currentSection); - const section = new Section(sectionId, currentSection.trim(), sectionType); - sections.push(section); - this.sections.set(sectionId, section); - position++; - } - } - - this.emit('sections-created', { sections, count: sections.length }); - return sections; - } - - startEditing(sectionId) { - debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER'); - - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - if (section.isEditing()) { - debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER'); - return section.editingMarkdown; - } - - debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER'); - const content = section.startEdit(); - - debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER'); - this.emit('edit-started', { sectionId, content, section: section.getStatus() }); - debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER'); - - return content; - } - - updateContent(sectionId, markdown) { - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - const oldType = section.type; - section.updateContent(markdown); - const newType = section.redetectType(markdown); - - const eventData = { - sectionId, - markdown, - section: section.getStatus(), - typeChanged: oldType !== newType, - oldType, - newType - }; - - this.emit('content-updated', eventData); - - if (oldType !== newType) { - this.emit('section-type-changed', { - sectionId, - oldType, - newType, - section: section.getStatus() - }); - } - } - - acceptChanges(sectionId) { - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - const content = section.acceptChanges(); - this.emit('changes-accepted', { sectionId, content, section: section.getStatus() }); - return content; - } - - cancelChanges(sectionId) { - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - const content = section.cancelChanges(); - this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() }); - return content; - } - - resetSection(sectionId) { - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - const content = section.resetToOriginal(); - this.emit('section-reset', { sectionId, content, section: section.getStatus() }); - return content; - } - - getDocumentMarkdown() { - const sortedSections = Array.from(this.sections.values()) - .sort((a, b) => a.created - b.created); - - return sortedSections.map(section => section.currentMarkdown).join('\n\n'); - } - - getAllSections() { - return Array.from(this.sections.values()); - } - - getDocumentStatus() { - const sections = Array.from(this.sections.values()); - const editingSections = sections.filter(section => section.isEditing).length; - - return { - totalSections: sections.length, - editingSections: editingSections - }; - } - - extractHeadings(content) { - if (!content) return []; - const lines = content.split('\n'); - return lines.filter(line => /^#{1,6}\s/.test(line.trim())); - } - - handleSectionSplit(sectionId, newContent) { - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - // Remove the original section - this.sections.delete(sectionId); - - // Create new sections from the content - const newSections = this.createSectionsFromMarkdown(newContent); - - // Emit section-split event - this.emit('section-split', { - originalSectionId: sectionId, - newSections: newSections, - count: newSections.length - }); - - return newSections; - } - - createSectionsFromContent(content) { - return this.createSectionsFromMarkdown(content); - } -} - -// Export for use in tests and other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { SectionManager, Section, EditState, SectionType }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.SectionManager = SectionManager; - window.Section = Section; - window.EditState = EditState; - window.SectionType = SectionType; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/refactor-test-runner.js b/testdrive-jsui/static/js/tests/refactor-test-runner.js deleted file mode 100644 index ecc97529..00000000 --- a/testdrive-jsui/static/js/tests/refactor-test-runner.js +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test Runner for JavaScript Refactoring - * - * Drives component extraction and testing during architecture refactoring. - * Ensures all functionality remains stable while achieving separation of concerns. - */ - -class RefactorTestRunner { - constructor() { - this.tests = []; - this.passed = 0; - this.failed = 0; - this.currentSuite = null; - this.setupDOM(); - } - - setupDOM() { - // Set up minimal DOM environment for testing - if (typeof document === 'undefined') { - const { JSDOM } = require('jsdom'); - const dom = new JSDOM('', { - url: 'http://localhost', - pretendToBeVisual: true, - resources: 'usable' - }); - - global.window = dom.window; - global.document = dom.window.document; - global.HTMLElement = dom.window.HTMLElement; - global.Event = dom.window.Event; - global.CustomEvent = dom.window.CustomEvent; - - // Only set navigator if it doesn't exist - if (typeof global.navigator === 'undefined') { - global.navigator = dom.window.navigator; - } - } - } - - describe(suiteName, fn) { - console.log(`\n๐Ÿ“ ${suiteName}`); - this.currentSuite = suiteName; - fn(); - this.currentSuite = null; - } - - it(testName, fn) { - const fullName = this.currentSuite ? `${this.currentSuite}: ${testName}` : testName; - - try { - fn(); - console.log(` โœ… ${testName}`); - this.passed++; - } catch (error) { - console.log(` โŒ ${testName}`); - console.log(` Error: ${error.message}`); - if (error.stack) { - console.log(` Stack: ${error.stack.split('\n')[1]?.trim()}`); - } - this.failed++; - } - } - - expect(actual) { - return { - toBe: (expected) => { - if (actual !== expected) { - throw new Error(`Expected ${expected}, got ${actual}`); - } - }, - toBeTruthy: () => { - if (!actual) { - throw new Error(`Expected truthy value, got ${actual}`); - } - }, - toBeFalsy: () => { - if (actual) { - throw new Error(`Expected falsy value, got ${actual}`); - } - }, - toEqual: (expected) => { - if (JSON.stringify(actual) !== JSON.stringify(expected)) { - throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); - } - }, - toContain: (expected) => { - if (!actual.includes(expected)) { - throw new Error(`Expected ${actual} to contain ${expected}`); - } - }, - toHaveProperty: (property) => { - if (!(property in actual)) { - throw new Error(`Expected object to have property ${property}`); - } - }, - toBeInstanceOf: (expectedClass) => { - if (!(actual instanceof expectedClass)) { - throw new Error(`Expected instance of ${expectedClass.name}, got ${actual.constructor.name}`); - } - } - }; - } - - /** - * Test that a component can be extracted from the monolith without breaking functionality - */ - testComponentExtraction(componentName, extractFn, originalTests) { - this.describe(`Component Extraction: ${componentName}`, () => { - this.it('should extract without syntax errors', () => { - try { - const component = extractFn(); - this.expect(component).toBeTruthy(); - } catch (error) { - throw new Error(`Component extraction failed: ${error.message}`); - } - }); - - this.it('should maintain original API', () => { - const component = extractFn(); - originalTests.forEach(test => { - try { - test(component); - } catch (error) { - throw new Error(`API compatibility test failed: ${error.message}`); - } - }); - }); - }); - } - - /** - * Test component integration after extraction - */ - testComponentIntegration(components, integrationTests) { - this.describe('Component Integration', () => { - integrationTests.forEach((test, index) => { - this.it(`integration test ${index + 1}`, () => { - test(components); - }); - }); - }); - } - - /** - * Setup test environment with mock dependencies - */ - setupTestEnvironment() { - // Create test container - const container = document.createElement('div'); - container.id = 'test-container'; - container.innerHTML = '
'; - document.body.appendChild(container); - - // Mock any global dependencies - global.mockSectionManager = { - sections: new Map(), - createSectionsFromMarkdown: () => [], - startEditing: () => true, - stopEditing: () => true, - getAllSections: () => [] - }; - - return { container }; - } - - /** - * Cleanup test environment - */ - cleanupTestEnvironment() { - const container = document.getElementById('test-container'); - if (container) { - container.remove(); - } - - // Clear any global mocks - delete global.mockSectionManager; - } - - async run() { - console.log('๐Ÿงช TDD Refactoring Test Runner Starting...\n'); - - const startTime = Date.now(); - - // Run all collected tests - // Tests will be added by importing component test files - - const endTime = Date.now(); - const duration = endTime - startTime; - - console.log(`\n๐Ÿ“Š Test Results:`); - console.log(` โœ… Passed: ${this.passed}`); - console.log(` โŒ Failed: ${this.failed}`); - console.log(` โฑ๏ธ Duration: ${duration}ms`); - - if (this.failed > 0) { - console.log(`\nโŒ ${this.failed} test(s) failed. Refactoring should not proceed.`); - process.exit(1); - } else { - console.log(`\nโœ… All tests passed! Refactoring is safe to continue.`); - } - } -} - -// Export for use in component tests -if (typeof module !== 'undefined' && module.exports) { - module.exports = { RefactorTestRunner }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.RefactorTestRunner = RefactorTestRunner; -} - -module.exports = RefactorTestRunner; \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-component-integration.js b/testdrive-jsui/static/js/tests/test-component-integration.js deleted file mode 100644 index 2107dc99..00000000 --- a/testdrive-jsui/static/js/tests/test-component-integration.js +++ /dev/null @@ -1,521 +0,0 @@ -#!/usr/bin/env node - -/** - * Comprehensive Component Integration Test - * - * Tests that extracted components work together properly. - * Verifies the complete workflow: Section Creation โ†’ Rendering โ†’ Editing โ†’ Saving - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Component Integration Tests', () => { - - runner.it('should load all extracted components', () => { - try { - // Load extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - - runner.expect(sectionModule.SectionManager).toBeTruthy(); - runner.expect(sectionModule.Section).toBeTruthy(); - runner.expect(domModule.DOMRenderer).toBeTruthy(); - runner.expect(domModule.FloatingMenu).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = sectionModule.SectionManager; - global.ExtractedSection = sectionModule.Section; - global.ExtractedDOMRenderer = domModule.DOMRenderer; - global.ExtractedFloatingMenu = domModule.FloatingMenu; - global.ExtractedEditState = sectionModule.EditState; - - } catch (error) { - throw new Error(`Failed to load extracted components: ${error.message}`); - } - }); - - runner.it('should support complete section creation workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test workflow: Create sections from markdown - const testMarkdown = `# Main Heading -This is the introduction content. - -## Subheading One -Content for first subsection. - -![Test Image](https://example.com/image.jpg) - -## Subheading Two -Content for second subsection.`; - - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - - // Verify sections were created - // Expected: heading+paragraph, heading+paragraph, image, heading+paragraph = 4 sections - runner.expect(sections.length).toBe(4); - runner.expect(sections[0].type).toBe('heading'); - runner.expect(sections[2].type).toBe('image'); - - // Verify DOM rendering - domRenderer.renderAllSections(sections); - const renderedElements = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedElements.length).toBe(sections.length); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support complete editing workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const EditState = global.ExtractedEditState; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nOriginal content here.'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const section = sectionManager.sections.get(sectionId); - - // Test workflow: Start editing - runner.expect(section.state).toBe(EditState.ORIGINAL); - runner.expect(section.isEditing()).toBeFalsy(); - - const content = sectionManager.startEditing(sectionId); - runner.expect(content).toContain('Test Heading'); - runner.expect(section.isEditing()).toBeTruthy(); - runner.expect(section.state).toBe(EditState.EDITING); - - // Test workflow: Update content - const newContent = '# Updated Heading\nModified content here.'; - sectionManager.updateContent(sectionId, newContent); - runner.expect(section.editingMarkdown).toBe(newContent); - - // Test workflow: Accept changes - sectionManager.acceptChanges(sectionId); - runner.expect(section.currentMarkdown).toBe(newContent); - runner.expect(section.state).toBe(EditState.SAVED); - runner.expect(section.isEditing()).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support accept/cancel button functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nOriginal content here.'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const section = sectionManager.sections.get(sectionId); - - // Start editing to trigger floating menu with buttons - sectionManager.startEditing(sectionId); - - // Check if floating menu exists - runner.expect(domRenderer.currentFloatingMenu).toBeTruthy(); - runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy(); - - // Find buttons in the floating menu - const menuElement = domRenderer.currentFloatingMenu.element; - runner.expect(menuElement).toBeTruthy(); - - const buttons = menuElement.querySelectorAll('button'); - runner.expect(buttons.length >= 2).toBeTruthy(); // At least Accept and Cancel buttons - - const acceptBtn = Array.from(buttons).find(btn => btn.textContent === 'Accept'); - const cancelBtn = Array.from(buttons).find(btn => btn.textContent === 'Cancel'); - - runner.expect(acceptBtn).toBeTruthy(); - runner.expect(cancelBtn).toBeTruthy(); - - // Test Accept button functionality - runner.expect(section.isEditing()).toBeTruthy(); - - // Simulate updating content and clicking Accept - const textarea = menuElement.querySelector('textarea'); - runner.expect(textarea).toBeTruthy(); - textarea.value = '# Updated Heading\nUpdated content via button.'; - - acceptBtn.click(); - - // After clicking Accept, section should be saved and menu hidden - runner.expect(section.isEditing()).toBeFalsy(); - runner.expect(section.currentMarkdown).toContain('Updated Heading'); - runner.expect(domRenderer.currentFloatingMenu).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support cancel button functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Original Heading\nOriginal content here.'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const section = sectionManager.sections.get(sectionId); - - // Start editing - sectionManager.startEditing(sectionId); - - // Find buttons in the floating menu - const menuElement = domRenderer.currentFloatingMenu.element; - const cancelBtn = Array.from(menuElement.querySelectorAll('button')).find(btn => btn.textContent === 'Cancel'); - - runner.expect(cancelBtn).toBeTruthy(); - runner.expect(section.isEditing()).toBeTruthy(); - - // Simulate changing content but then canceling - const textarea = menuElement.querySelector('textarea'); - textarea.value = '# Changed Heading\nThis should be discarded.'; - - cancelBtn.click(); - - // After clicking Cancel, section should not be saved and menu hidden - runner.expect(section.isEditing()).toBeFalsy(); - runner.expect(section.currentMarkdown).toContain('Original Heading'); // Original content preserved - runner.expect(domRenderer.currentFloatingMenu).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support event-driven communication', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Track events - let sectionsCreatedEvent = null; - let editStartedEvent = null; - - sectionManager.on('sections-created', (data) => { - sectionsCreatedEvent = data; - }); - - sectionManager.on('edit-started', (data) => { - editStartedEvent = data; - }); - - // Test event: sections-created - const testMarkdown = '# Test\nContent'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - runner.expect(sectionsCreatedEvent).toBeTruthy(); - runner.expect(sectionsCreatedEvent.sections).toEqual(sections); - runner.expect(sectionsCreatedEvent.count).toBe(1); - - // Test event: edit-started - const sectionId = sections[0].id; - sectionManager.startEditing(sectionId); - - runner.expect(editStartedEvent).toBeTruthy(); - runner.expect(editStartedEvent.sectionId).toBe(sectionId); - runner.expect(editStartedEvent.content).toContain('Test'); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support section type detection and rendering', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const Section = global.ExtractedSection; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test different section types - const testMarkdown = `# Heading Section -Regular paragraph content. - -![Image Section](https://example.com/test.jpg) - -\`\`\`javascript -// Code section -console.log('test'); -\`\`\``; - - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - - // Verify type detection - adjusted for actual parsing behavior - // Expected: heading+paragraph, image, code = 3 sections - runner.expect(sections[0].type).toBe('heading'); // Combined heading+paragraph - runner.expect(sections[1].type).toBe('image'); // Image section - runner.expect(sections[2].type).toBe('code'); // Code section - - // Verify image detection - runner.expect(sections[1].isImage()).toBeTruthy(); // Image is now at index 1 - runner.expect(sections[0].isImage()).toBeFalsy(); - - // Verify rendering handles different types - domRenderer.renderAllSections(sections); - const renderedElements = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedElements.length).toBe(sections.length); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support FloatingMenu integration', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const FloatingMenu = global.ExtractedFloatingMenu; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - - // Test showing editor (which uses FloatingMenu) - domRenderer.showEditor(sectionId, 'test content'); - - // Verify floating menu state - runner.expect(domRenderer.currentFloatingMenu).toBeTruthy(); - runner.expect(domRenderer.currentFloatingMenu.sectionId).toBe(sectionId); - runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy(); - runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy(); - - // Test hiding editor - domRenderer.hideCurrentEditor(); - runner.expect(domRenderer.currentFloatingMenu).toBeFalsy(); - runner.expect(domRenderer.editingSections.has(sectionId)).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support complete click-to-edit workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nTest content for editing'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = domRenderer.findSectionElement(sectionId); - - // Simulate click event - const clickEvent = new Event('click', { bubbles: true }); - Object.defineProperty(clickEvent, 'target', { value: element }); - - // Test complete workflow - domRenderer.handleSectionClick(clickEvent); - - // Verify editing state was triggered - const section = sectionManager.sections.get(sectionId); - runner.expect(section.isEditing()).toBeTruthy(); - runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy(); - runner.expect(domRenderer.currentFloatingMenu).toBeTruthy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support document status tracking', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - const sectionManager = new SectionManager(); - const container = document.createElement('div'); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test initial status - let status = sectionManager.getDocumentStatus(); - runner.expect(status.totalSections).toBe(0); - runner.expect(status.editingSections).toBe(0); - - // Create sections - const testMarkdown = '# Section 1\nContent 1\n\n# Section 2\nContent 2'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - status = sectionManager.getDocumentStatus(); - runner.expect(status.totalSections).toBe(2); - runner.expect(status.editingSections).toBe(2); // Bug compatibility (isEditing property exists) - - // Test getAllSections - const allSections = sectionManager.getAllSections(); - runner.expect(allSections.length).toBe(2); - runner.expect(allSections[0].currentMarkdown).toContain('Section 1'); - runner.expect(allSections[1].currentMarkdown).toContain('Section 2'); - }); - - runner.it('should support event tracking and analytics', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test event tracking - domRenderer.trackEvent('test-event', { data: 'test' }); - domRenderer.trackEvent('section-click', { sectionId: 'test-123' }); - - const stats = domRenderer.getEventStats(); - runner.expect(stats.totalEvents).toBe(1); // Only section-click is tracked in stats - runner.expect(stats.stats['section-click']).toBe(1); - runner.expect(stats.recentEvents.length).toBe(2); - runner.expect(stats.recentEvents[0].type).toBe('test-event'); - runner.expect(stats.recentEvents[1].type).toBe('section-click'); - }); - - // Integration stress test - runner.it('should handle complex document with multiple operations', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Complex document - const complexMarkdown = `# Document Title -Introduction paragraph with some content. - -## Section A -Content for section A with details. - -![Test Image](https://example.com/test.jpg) - -### Subsection A.1 -More detailed content here. - -\`\`\`javascript -function test() { - console.log('code block'); -} -\`\`\` - -## Section B -Final section content.`; - - // Create and render - const sections = sectionManager.createSectionsFromMarkdown(complexMarkdown); - domRenderer.renderAllSections(sections); - - runner.expect(sections.length).toBe(6); // Adjusted based on actual parsing - - // Test editing multiple sections - const firstSection = sections[0]; - const imageSection = sections.find(s => s.isImage()); - const codeSection = sections.find(s => s.type === 'code'); - - // Edit first section - sectionManager.startEditing(firstSection.id); - sectionManager.updateContent(firstSection.id, '# Updated Title\nUpdated intro.'); - sectionManager.acceptChanges(firstSection.id); - - // Edit image section - sectionManager.startEditing(imageSection.id); - sectionManager.updateContent(imageSection.id, '![Updated Image](https://example.com/new.jpg)'); - sectionManager.acceptChanges(imageSection.id); - - // Verify changes - runner.expect(firstSection.currentMarkdown).toContain('Updated Title'); - runner.expect(imageSection.currentMarkdown).toContain('Updated Image'); - - // Verify document reconstruction - const finalMarkdown = sectionManager.getDocumentMarkdown(); - runner.expect(finalMarkdown).toContain('Updated Title'); - runner.expect(finalMarkdown).toContain('Updated Image'); - runner.expect(finalMarkdown).toContain('Section B'); - - // Cleanup - document.body.removeChild(container); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running Component Integration Tests'); - runner.run().then(() => { - console.log('โœ… Component integration tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-debugpanel-extraction.js b/testdrive-jsui/static/js/tests/test-debugpanel-extraction.js deleted file mode 100644 index 5dca6cae..00000000 --- a/testdrive-jsui/static/js/tests/test-debugpanel-extraction.js +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Debug Panel Component Extraction - * - * Tests the extraction of DebugPanel from the monolithic editor.js - * DebugPanel handles debug message display and management. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// Define expected DebugPanel API -const EXPECTED_DEBUGPANEL_API = [ - 'constructor', - 'toggle', - 'update', - 'clear', - 'addMessage', - 'show', - 'hide', - 'getMessageCount', - 'getRecentMessages' -]; - -runner.describe('DebugPanel Component Extraction', () => { - - runner.it('should define expected API methods', () => { - const expectedMethods = EXPECTED_DEBUGPANEL_API; - runner.expect(expectedMethods.length).toBe(9); - runner.expect(expectedMethods).toContain('toggle'); - runner.expect(expectedMethods).toContain('update'); - runner.expect(expectedMethods).toContain('addMessage'); - }); - - runner.it('should load extracted DebugPanel component', () => { - // Load the extracted component - delete require.cache[require.resolve('../components/debug-panel.js')]; - - try { - const module = require('../components/debug-panel.js'); - runner.expect(module.DebugPanel).toBeTruthy(); - - // Set global for other tests - global.ExtractedDebugPanel = module.DebugPanel; - } catch (error) { - throw new Error(`Failed to load extracted DebugPanel: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - runner.expect(debugPanel).toBeInstanceOf(DebugPanel); - runner.expect(debugPanel.messages).toBeInstanceOf(Array); - runner.expect(debugPanel.isActive).toBeFalsy(); - }); - - runner.it('should preserve message handling functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Test adding messages - debugPanel.addMessage('Test message', 'INFO'); - runner.expect(debugPanel.getMessageCount()).toBe(1); - - const recentMessages = debugPanel.getRecentMessages(1); - runner.expect(recentMessages.length).toBe(1); - runner.expect(recentMessages[0].message).toBe('Test message'); - runner.expect(recentMessages[0].category).toBe('INFO'); - }); - - runner.it('should preserve toggle functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - // Create container element - const container = document.createElement('div'); - container.id = 'debug-messages-container'; - container.style.display = 'none'; - document.body.appendChild(container); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - document.body.appendChild(debugButton); - - const debugPanel = new DebugPanel(); - - // Test toggle on - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeTruthy(); - - // Test toggle off - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - document.body.removeChild(debugButton); - }); - - runner.it('should preserve update functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const container = document.createElement('div'); - container.id = 'debug-messages-container'; - document.body.appendChild(container); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - document.body.appendChild(debugButton); - - const debugPanel = new DebugPanel(); - debugPanel.show(); - - debugPanel.addMessage('Test message 1', 'INFO'); - debugPanel.addMessage('Test message 2', 'ERROR'); - debugPanel.update(); - - runner.expect(container.innerHTML.length > 100).toBeTruthy(); - runner.expect(container.innerHTML).toContain('Test message 1'); - runner.expect(container.innerHTML).toContain('Test message 2'); - - // Cleanup - document.body.removeChild(container); - document.body.removeChild(debugButton); - }); - - runner.it('should preserve clear functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - debugPanel.addMessage('Test message 1', 'INFO'); - debugPanel.addMessage('Test message 2', 'ERROR'); - runner.expect(debugPanel.getMessageCount()).toBe(2); - - debugPanel.clear(); - runner.expect(debugPanel.getMessageCount()).toBe(0); - }); - - runner.it('should have core debug panel methods', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Should have core methods - runner.expect(typeof debugPanel.toggle === 'function').toBeTruthy(); - runner.expect(typeof debugPanel.update === 'function').toBeTruthy(); - runner.expect(typeof debugPanel.addMessage === 'function').toBeTruthy(); - runner.expect(typeof debugPanel.clear === 'function').toBeTruthy(); - }); - - runner.it('should handle message categories properly', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Test different message categories - debugPanel.addMessage('Info message', 'INFO'); - debugPanel.addMessage('Warning message', 'WARNING'); - debugPanel.addMessage('Error message', 'ERROR'); - debugPanel.addMessage('Success message', 'SUCCESS'); - - const messages = debugPanel.getRecentMessages(4); - runner.expect(messages.length).toBe(4); - - const categories = messages.map(m => m.category); - runner.expect(categories).toContain('INFO'); - runner.expect(categories).toContain('WARNING'); - runner.expect(categories).toContain('ERROR'); - runner.expect(categories).toContain('SUCCESS'); - }); -}); - -module.exports = { - runner, - EXPECTED_DEBUGPANEL_API -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing DebugPanel Component Extraction'); - runner.run().then(() => { - console.log('โœ… DebugPanel extraction tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-debugpanel-integration.js b/testdrive-jsui/static/js/tests/test-debugpanel-integration.js deleted file mode 100644 index af03ff83..00000000 --- a/testdrive-jsui/static/js/tests/test-debugpanel-integration.js +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env node - -/** - * DebugPanel Integration Test - * - * Tests that the extracted DebugPanel component integrates properly - * with the existing SectionManager and DOMRenderer components. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('DebugPanel Integration Tests', () => { - - runner.it('should load all extracted components including DebugPanel', () => { - try { - // Load extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - - runner.expect(sectionModule.SectionManager).toBeTruthy(); - runner.expect(domModule.DOMRenderer).toBeTruthy(); - runner.expect(debugModule.DebugPanel).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = sectionModule.SectionManager; - global.ExtractedDOMRenderer = domModule.DOMRenderer; - global.ExtractedDebugPanel = debugModule.DebugPanel; - - } catch (error) { - throw new Error(`Failed to load extracted components: ${error.message}`); - } - }); - - runner.it('should support debug panel with section editing workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - - // Setup DOM elements - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const debugContainer = document.createElement('div'); - debugContainer.id = 'debug-messages-container'; - debugContainer.style.display = 'none'; - document.body.appendChild(debugContainer); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - document.body.appendChild(debugButton); - - // Create components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - - // Test workflow: Create sections and debug them - const testMarkdown = '# Test Heading\nTest content for debugging'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - // Add debug messages - debugPanel.addMessage('Section created: ' + sections[0].id, 'INFO'); - debugPanel.addMessage('DOM rendered successfully', 'SUCCESS'); - - runner.expect(debugPanel.getMessageCount()).toBe(2); - - // Test showing debug panel - debugPanel.show(); - runner.expect(debugPanel.isActive).toBeTruthy(); - - // Test debug panel content - const messages = debugPanel.getRecentMessages(2); - runner.expect(messages[0].message).toContain('Section created'); - runner.expect(messages[1].message).toContain('DOM rendered'); - - // Cleanup - document.body.removeChild(container); - document.body.removeChild(debugContainer); - document.body.removeChild(debugButton); - }); - - runner.it('should support debug panel clearing and message management', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Add multiple messages - for (let i = 0; i < 10; i++) { - debugPanel.addMessage(`Test message ${i}`, i % 2 === 0 ? 'INFO' : 'WARNING'); - } - - runner.expect(debugPanel.getMessageCount()).toBe(10); - - // Test getting recent messages - const recentFive = debugPanel.getRecentMessages(5); - runner.expect(recentFive.length).toBe(5); - runner.expect(recentFive[4].message).toContain('Test message 9'); - - // Test clearing - debugPanel.clear(); - runner.expect(debugPanel.getMessageCount()).toBe(0); - }); - - runner.it('should handle debug panel DOM integration properly', () => { - const DebugPanel = global.ExtractedDebugPanel; - - // Setup DOM - const debugContainer = document.createElement('div'); - debugContainer.id = 'debug-messages-container'; - debugContainer.style.display = 'none'; - document.body.appendChild(debugContainer); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - debugButton.style.background = '#6c757d'; - document.body.appendChild(debugButton); - - const debugPanel = new DebugPanel(); - - // Test initial state - runner.expect(debugPanel.isActive).toBeFalsy(); - runner.expect(debugContainer.style.display).toBe('none'); - - // Test toggle on - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeTruthy(); - runner.expect(debugContainer.style.display).toBe('block'); - runner.expect(debugButton.textContent).toContain('Debug (ON)'); - - // Test toggle off - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeFalsy(); - runner.expect(debugContainer.style.display).toBe('none'); - runner.expect(debugButton.textContent).toBe('๐Ÿ” Debug'); - - // Cleanup - document.body.removeChild(debugContainer); - document.body.removeChild(debugButton); - }); - - runner.it('should handle missing DOM elements gracefully', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Try to toggle without DOM elements (should not throw) - try { - debugPanel.toggle(); - debugPanel.show(); - debugPanel.hide(); - debugPanel.update(); - runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown - } catch (error) { - throw new Error(`DebugPanel should handle missing DOM gracefully: ${error.message}`); - } - }); - - runner.it('should support event-driven debug message addition', () => { - const SectionManager = global.ExtractedSectionManager; - const DebugPanel = global.ExtractedDebugPanel; - - const sectionManager = new SectionManager(); - const debugPanel = new DebugPanel(); - - // Listen to section manager events and add debug messages - let eventCount = 0; - - sectionManager.on('sections-created', (data) => { - debugPanel.addMessage(`Sections created: ${data.count} sections`, 'INFO'); - eventCount++; - }); - - sectionManager.on('edit-started', (data) => { - debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG'); - eventCount++; - }); - - // Create sections - const testMarkdown = '# Test\nContent'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - // Start editing - sectionManager.startEditing(sections[0].id); - - // Verify debug messages were added - runner.expect(eventCount).toBe(2); - runner.expect(debugPanel.getMessageCount()).toBe(2); - - const messages = debugPanel.getRecentMessages(2); - runner.expect(messages[0].message).toContain('Sections created'); - runner.expect(messages[1].message).toContain('Edit started'); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running DebugPanel Integration Tests'); - runner.run().then(() => { - console.log('โœ… DebugPanel integration tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-document-navigator-runner.html b/testdrive-jsui/static/js/tests/test-document-navigator-runner.html deleted file mode 100644 index 2c8a7621..00000000 --- a/testdrive-jsui/static/js/tests/test-document-navigator-runner.html +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - DocumentNavigator TDD Test Runner - - - -
-

๐Ÿ“‹ DocumentNavigator Widget TDD Test Suite

-

- This test suite follows Test-Driven Development methodology to implement a Substack-style - floating document navigation widget. The tests define the expected behavior before - implementation begins. -

- -
- Test Coverage: -
    -
  • โœ… Widget class structure and inheritance
  • -
  • โœ… Configuration and initialization
  • -
  • โœ… DOM rendering and UI elements
  • -
  • โœ… Heading extraction and hierarchy building
  • -
  • โœ… Navigation functionality and smooth scrolling
  • -
  • โœ… Expand/collapse behavior
  • -
  • โœ… Scroll spy and active section detection
  • -
  • โœ… Responsive behavior and auto-hide
  • -
  • โœ… Keyboard navigation support
  • -
  • โœ… Event emission and user interaction
  • -
  • โœ… Edge cases and error handling
  • -
-
- - - - -
- - - - - - - - - \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-document-navigator.js b/testdrive-jsui/static/js/tests/test-document-navigator.js deleted file mode 100644 index e6a79f99..00000000 --- a/testdrive-jsui/static/js/tests/test-document-navigator.js +++ /dev/null @@ -1,432 +0,0 @@ -/** - * TDD Test Suite for DocumentNavigator Widget - * - * Tests the Substack-style floating navigation widget for document headings. - * Following TDD methodology: write tests first, then implement functionality. - */ - -// Simple test runner for browser environment -class DocumentNavigatorTestRunner { - constructor() { - this.tests = []; - this.results = { - passed: 0, - failed: 0, - total: 0 - }; - } - - test(name, testFn) { - this.tests.push({ name, testFn }); - } - - expect(actual) { - return { - toBe: (expected) => { - if (actual !== expected) { - throw new Error(`Expected ${actual} to be ${expected}`); - } - }, - toBeInstanceOf: (expectedClass) => { - if (!(actual instanceof expectedClass)) { - throw new Error(`Expected ${actual} to be instance of ${expectedClass.name}`); - } - }, - toBeTruthy: () => { - if (!actual) { - throw new Error(`Expected ${actual} to be truthy`); - } - }, - toBeFalsy: () => { - if (actual) { - throw new Error(`Expected ${actual} to be falsy`); - } - }, - toContain: (expected) => { - if (typeof actual === 'string' && !actual.includes(expected)) { - throw new Error(`Expected "${actual}" to contain "${expected}"`); - } - if (Array.isArray(actual) && !actual.includes(expected)) { - throw new Error(`Expected array to contain ${expected}`); - } - }, - toHaveLength: (expected) => { - if (actual.length !== expected) { - throw new Error(`Expected length ${actual.length} to be ${expected}`); - } - }, - toBeGreaterThan: (expected) => { - if (actual <= expected) { - throw new Error(`Expected ${actual} to be greater than ${expected}`); - } - } - }; - } - - async run() { - console.log('๐Ÿงช Running DocumentNavigator TDD Test Suite...\n'); - - for (const { name, testFn } of this.tests) { - this.results.total++; - - try { - await testFn.call(this); - this.results.passed++; - console.log(`โœ… ${name}`); - } catch (error) { - this.results.failed++; - console.log(`โŒ ${name}`); - console.log(` ${error.message}\n`); - } - } - - this.printSummary(); - } - - printSummary() { - console.log(`\n๐Ÿ“Š Test Results:`); - console.log(` Passed: ${this.results.passed}`); - console.log(` Failed: ${this.results.failed}`); - console.log(` Total: ${this.results.total}`); - - if (this.results.failed === 0) { - console.log(`\n๐ŸŽ‰ All tests passed!`); - } else { - console.log(`\nโŒ ${this.results.failed} test(s) failed.`); - } - } -} - -// Create test runner -const runner = new DocumentNavigatorTestRunner(); - -// Test Suite: DocumentNavigator Widget -runner.test('DocumentNavigator class should exist and be importable', async function() { - // This test will fail initially - we haven't created the class yet - try { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - this.expect(DocumentNavigator).toBeTruthy(); - this.expect(typeof DocumentNavigator).toBe('function'); - } catch (error) { - throw new Error(`DocumentNavigator class not found: ${error.message}`); - } -}); - -runner.test('DocumentNavigator should extend UIWidget', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - const { UIWidget } = await import('../widgets/base/UIWidget.js'); - - const navigator = new DocumentNavigator(); - this.expect(navigator).toBeInstanceOf(UIWidget); -}); - -runner.test('DocumentNavigator should initialize with default configuration', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - - // Test default configuration - this.expect(navigator.config.position).toBe('left'); - this.expect(navigator.config.collapsed).toBe(true); - this.expect(navigator.config.autoHide).toBe(true); - this.expect(navigator.config.maxHeadingLevel).toBe(3); - this.expect(navigator.config.enableScrollSpy).toBe(true); -}); - -runner.test('DocumentNavigator should accept custom configuration', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const customConfig = { - position: 'right', - collapsed: false, - maxHeadingLevel: 4, - theme: 'dark' - }; - - const navigator = new DocumentNavigator(customConfig); - - this.expect(navigator.config.position).toBe('right'); - this.expect(navigator.config.collapsed).toBe(false); - this.expect(navigator.config.maxHeadingLevel).toBe(4); - this.expect(navigator.config.theme).toBe('dark'); -}); - -runner.test('DocumentNavigator should render floating panel element', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - await navigator.render(); - - this.expect(navigator.element).toBeInstanceOf(HTMLElement); - this.expect(navigator.element.classList.contains('document-navigator')).toBeTruthy(); - this.expect(navigator.element.style.position).toBe('fixed'); -}); - -runner.test('DocumentNavigator should have toggle button in collapsed state', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator({ collapsed: true }); - await navigator.render(); - - const toggleButton = navigator.findElement('.navigator-toggle'); - this.expect(toggleButton).toBeInstanceOf(HTMLElement); - this.expect(toggleButton.style.display).not.toBe('none'); - - const navList = navigator.findElement('.navigator-list'); - this.expect(navList.style.display).toBe('none'); -}); - -runner.test('DocumentNavigator should extract headings from document', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document with headings - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -

First Heading

-

Some content

-

Second Heading

-

Third Heading

-

More content

-

Fourth Heading

- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ - container: testContainer, - maxHeadingLevel: 3 - }); - - const headings = navigator.extractHeadings(); - - this.expect(headings).toHaveLength(4); - this.expect(headings[0].tagName).toBe('H1'); - this.expect(headings[0].textContent).toBe('First Heading'); - this.expect(headings[1].tagName).toBe('H2'); - this.expect(headings[2].tagName).toBe('H3'); - this.expect(headings[3].tagName).toBe('H2'); - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should build navigation hierarchy', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document with nested headings - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -

Chapter 1

-

Section 1.1

-

Subsection 1.1.1

-

Subsection 1.1.2

-

Section 1.2

-

Chapter 2

- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ container: testContainer }); - await navigator.render(); - - const navItems = navigator.buildNavigationTree(); - - // Should have hierarchical structure - this.expect(navItems).toHaveLength(2); // 2 H1 elements - this.expect(navItems[0].children).toHaveLength(2); // 2 H2 under first H1 - this.expect(navItems[0].children[0].children).toHaveLength(2); // 2 H3 under first H2 - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should handle click navigation', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -

Target Heading

-

Spacer content

- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ container: testContainer }); - await navigator.render(); - - // Simulate click on navigation item - const navItem = navigator.findElement('[data-target="target-heading"]'); - this.expect(navItem).toBeTruthy(); - - // Mock scrollIntoView for testing - const targetElement = document.getElementById('target-heading'); - let scrollCalled = false; - targetElement.scrollIntoView = () => { scrollCalled = true; }; - - // Click navigation item - navItem.click(); - - this.expect(scrollCalled).toBeTruthy(); - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should support expand/collapse functionality', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator({ collapsed: true }); - await navigator.render(); - - // Should start collapsed - this.expect(navigator.isCollapsed).toBeTruthy(); - - const toggleButton = navigator.findElement('.navigator-toggle'); - const navList = navigator.findElement('.navigator-list'); - - // Toggle to expanded - await navigator.expand(); - this.expect(navigator.isCollapsed).toBeFalsy(); - this.expect(navList.style.display).not.toBe('none'); - - // Toggle back to collapsed - await navigator.collapse(); - this.expect(navigator.isCollapsed).toBeTruthy(); - this.expect(navList.style.display).toBe('none'); -}); - -runner.test('DocumentNavigator should implement scroll spy functionality', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document with multiple sections - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -
-

Section 1

-
-

Section 2

-
-

Section 3

-
- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ - container: testContainer, - enableScrollSpy: true - }); - await navigator.render(); - - // Test current section detection - const currentSection = navigator.getCurrentSection(); - this.expect(currentSection).toBeTruthy(); - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should handle responsive behavior', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator({ autoHide: true }); - await navigator.render(); - - // Mock viewport resize - const originalInnerWidth = window.innerWidth; - - // Test mobile viewport - Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true }); - navigator.handleResize(); - this.expect(navigator.element.style.display).toBe('none'); - - // Test desktop viewport - Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true }); - navigator.handleResize(); - this.expect(navigator.element.style.display).not.toBe('none'); - - // Restore original - Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true }); -}); - -runner.test('DocumentNavigator should provide keyboard navigation support', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - await navigator.render(); - - // Test keyboard shortcuts - let expandCalled = false; - let collapseCalled = false; - - navigator.expand = async () => { expandCalled = true; }; - navigator.collapse = async () => { collapseCalled = true; }; - - // Simulate keyboard events - const element = navigator.element; - - // Test Escape key (should collapse) - const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }); - element.dispatchEvent(escapeEvent); - this.expect(collapseCalled).toBeTruthy(); - - // Test Enter/Space key (should expand) - const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); - element.dispatchEvent(enterEvent); - this.expect(expandCalled).toBeTruthy(); -}); - -runner.test('DocumentNavigator should emit events for user interactions', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - await navigator.render(); - - // Test event emission - let navigationEvent = null; - navigator.addEventListener('navigate', (e) => { - navigationEvent = e; - }); - - let toggleEvent = null; - navigator.addEventListener('toggle', (e) => { - toggleEvent = e; - }); - - // Trigger navigation - navigator.navigateToHeading('test-heading'); - this.expect(navigationEvent).toBeTruthy(); - this.expect(navigationEvent.detail.target).toBe('test-heading'); - - // Trigger toggle - await navigator.toggle(); - this.expect(toggleEvent).toBeTruthy(); -}); - -runner.test('DocumentNavigator should handle empty document gracefully', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create empty container - const emptyContainer = document.createElement('div'); - document.body.appendChild(emptyContainer); - - const navigator = new DocumentNavigator({ container: emptyContainer }); - - const headings = navigator.extractHeadings(); - this.expect(headings).toHaveLength(0); - - await navigator.render(); - const navList = navigator.findElement('.navigator-list'); - this.expect(navList.children).toHaveLength(0); - - // Should show empty state message - const emptyMessage = navigator.findElement('.navigator-empty'); - this.expect(emptyMessage).toBeTruthy(); - - // Cleanup - document.body.removeChild(emptyContainer); -}); - -// Export test runner for use in HTML -window.runDocumentNavigatorTests = () => runner.run(); - -console.log('๐Ÿ“‹ DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()'); - -export { runner }; \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-documentcontrols-extraction.js b/testdrive-jsui/static/js/tests/test-documentcontrols-extraction.js deleted file mode 100644 index 2d5607ca..00000000 --- a/testdrive-jsui/static/js/tests/test-documentcontrols-extraction.js +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Document Controls Component Extraction - * - * Tests the extraction of DocumentControls from the monolithic editor.js - * DocumentControls handles the floating control panel and its actions. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// Define expected DocumentControls API -const EXPECTED_DOCUMENTCONTROLS_API = [ - 'constructor', - 'create', - 'destroy', - 'show', - 'hide', - 'addButton', - 'removeButton', - 'setEventHandlers', - 'updateStatus', - 'getControlPanel' -]; - -runner.describe('DocumentControls Component Extraction', () => { - - runner.it('should define expected API methods', () => { - const expectedMethods = EXPECTED_DOCUMENTCONTROLS_API; - runner.expect(expectedMethods.length).toBe(10); - runner.expect(expectedMethods).toContain('create'); - runner.expect(expectedMethods).toContain('addButton'); - runner.expect(expectedMethods).toContain('setEventHandlers'); - }); - - runner.it('should load extracted DocumentControls component', () => { - // Load the extracted component - delete require.cache[require.resolve('../components/document-controls.js')]; - - try { - const module = require('../components/document-controls.js'); - runner.expect(module.DocumentControls).toBeTruthy(); - - // Set global for other tests - global.ExtractedDocumentControls = module.DocumentControls; - } catch (error) { - throw new Error(`Failed to load extracted DocumentControls: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - runner.expect(controls).toBeInstanceOf(DocumentControls); - runner.expect(controls.controlPanel).toBeFalsy(); // Initially null - runner.expect(controls.buttons).toBeInstanceOf(Map); - }); - - runner.it('should preserve control panel creation functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - const panel = controls.getControlPanel(); - runner.expect(panel).toBeTruthy(); - runner.expect(panel.id).toBe('markitect-global-controls'); - - // Check that panel is added to DOM - const domPanel = document.getElementById('markitect-global-controls'); - runner.expect(domPanel).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should preserve button creation functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Default buttons should be created - runner.expect(controls.buttons.has('save-document')).toBeTruthy(); - runner.expect(controls.buttons.has('reset-all')).toBeTruthy(); - runner.expect(controls.buttons.has('show-status')).toBeTruthy(); - runner.expect(controls.buttons.has('toggle-debug')).toBeTruthy(); - - // Check DOM elements exist - runner.expect(document.getElementById('save-document')).toBeTruthy(); - runner.expect(document.getElementById('reset-all')).toBeTruthy(); - runner.expect(document.getElementById('show-status')).toBeTruthy(); - runner.expect(document.getElementById('toggle-debug')).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should support custom button addition', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Add custom button - const customButton = controls.addButton('custom-test', '๐ŸŽฏ Test', '#ff6600'); - runner.expect(customButton).toBeTruthy(); - runner.expect(customButton.id).toBe('custom-test'); - runner.expect(customButton.textContent).toBe('๐ŸŽฏ Test'); - - // Check button is in map and DOM - runner.expect(controls.buttons.has('custom-test')).toBeTruthy(); - runner.expect(document.getElementById('custom-test')).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should support event handler configuration', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - let saveClicked = false; - let resetClicked = false; - - const handlers = { - 'save-document': () => { saveClicked = true; }, - 'reset-all': () => { resetClicked = true; } - }; - - controls.setEventHandlers(handlers); - - // Simulate button clicks - const saveBtn = document.getElementById('save-document'); - const resetBtn = document.getElementById('reset-all'); - - saveBtn.click(); - resetBtn.click(); - - runner.expect(saveClicked).toBeTruthy(); - runner.expect(resetClicked).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should support show/hide functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - const panel = controls.getControlPanel(); - - // Test hiding - controls.hide(); - runner.expect(panel.style.display).toBe('none'); - - // Test showing - controls.show(); - runner.expect(panel.style.display).toBe('block'); - - // Cleanup - controls.destroy(); - }); - - runner.it('should preserve destroy functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Verify panel exists - runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy(); - - // Destroy - controls.destroy(); - - // Verify panel is removed - runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy(); - runner.expect(controls.controlPanel).toBeFalsy(); - }); - - runner.it('should support status updates', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Test status update - controls.updateStatus({ totalSections: 5, editingSections: 2 }); - - // The status should be reflected in the panel (implementation specific) - const panel = controls.getControlPanel(); - runner.expect(panel).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); -}); - -module.exports = { - runner, - EXPECTED_DOCUMENTCONTROLS_API -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing DocumentControls Component Extraction'); - runner.run().then(() => { - console.log('โœ… DocumentControls extraction tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-domrenderer-extraction.js b/testdrive-jsui/static/js/tests/test-domrenderer-extraction.js deleted file mode 100644 index e8aadc04..00000000 --- a/testdrive-jsui/static/js/tests/test-domrenderer-extraction.js +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for DOMRenderer Component Extraction - * - * Tests the extraction of DOMRenderer from the monolithic editor.js - * DOMRenderer handles all DOM interactions and UI rendering. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// Define expected DOMRenderer API -const EXPECTED_DOMRENDERER_API = [ - 'constructor', - 'renderAllSections', - 'renderSection', - 'showEditor', - 'hideCurrentEditor', - 'showImageEditor', - 'findSectionElement', - 'handleSectionClick', - 'setupSectionElement', - 'trackEvent', - 'getEventStats' - // Note: addGlobalControls and debug methods are on MarkitectCleanEditor, not DOMRenderer -]; - -runner.describe('DOMRenderer Component Extraction', () => { - - runner.it('should define expected API methods', () => { - const expectedMethods = EXPECTED_DOMRENDERER_API; - runner.expect(expectedMethods.length).toBe(11); - runner.expect(expectedMethods).toContain('renderAllSections'); - runner.expect(expectedMethods).toContain('showEditor'); - runner.expect(expectedMethods).toContain('handleSectionClick'); - }); - - runner.it('should extract from monolithic editor.js', () => { - // Load the monolithic editor.js to extract DOMRenderer - delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')]; - - try { - const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - runner.expect(editorModule.DOMRenderer).toBeTruthy(); - // Set global for other tests - global.DOMRenderer = editorModule.DOMRenderer; - global.SectionManager = editorModule.SectionManager; - } catch (error) { - throw new Error(`Failed to load monolithic editor.js: ${error.message}`); - } - }); - - runner.it('should preserve DOMRenderer constructor functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - - const renderer = new DOMRenderer(sectionManager, container); - runner.expect(renderer).toBeInstanceOf(DOMRenderer); - runner.expect(renderer.sectionManager).toBe(sectionManager); - runner.expect(renderer.container).toBe(container); - }); - - runner.it('should preserve section rendering functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - // This should not throw an error - renderer.renderAllSections(sections); - - // Check that some content was rendered - runner.expect(container.innerHTML.length).toBe(container.innerHTML.length); // Basic sanity check - }); - - runner.it('should preserve findSectionElement functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = renderer.findSectionElement(sectionId); - - // Should find an element or return null (not throw error) - runner.expect(typeof element === 'object').toBeTruthy(); - }); - - runner.it('should preserve event tracking functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - // Should have trackEvent method - runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy(); - - // Should be able to track an event - renderer.trackEvent('test-event', { data: 'test' }); - - // Should have getEventStats method - runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy(); - - const stats = renderer.getEventStats(); - runner.expect(typeof stats === 'object').toBeTruthy(); - }); - - runner.it('should preserve editor showing functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - - // showEditor should not throw error - try { - renderer.showEditor(sectionId, 'test content'); - runner.expect(true).toBeTruthy(); // If we get here, no error was thrown - } catch (error) { - // Some errors are expected if DOM structure isn't complete - runner.expect(typeof error.message === 'string').toBeTruthy(); - } - }); - - runner.it('should have core DOM rendering methods', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - // Should have core methods - runner.expect(typeof renderer.renderAllSections === 'function').toBeTruthy(); - runner.expect(typeof renderer.showEditor === 'function').toBeTruthy(); - runner.expect(typeof renderer.findSectionElement === 'function').toBeTruthy(); - runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy(); - }); -}); - -// Export API tests for use during extraction -const DOMRENDERER_API_TESTS = [ - (DOMRenderer, SectionManager) => { - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - if (!renderer.sectionManager) { - throw new Error('sectionManager property missing'); - } - }, - (DOMRenderer, SectionManager) => { - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - if (typeof renderer.renderAllSections !== 'function') { - throw new Error('renderAllSections method missing'); - } - }, - (DOMRenderer, SectionManager) => { - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - if (typeof renderer.showEditor !== 'function') { - throw new Error('showEditor method missing'); - } - } -]; - -module.exports = { - runner, - EXPECTED_DOMRENDERER_API, - DOMRENDERER_API_TESTS -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing DOMRenderer Component Extraction'); - runner.run().then(() => { - console.log('โœ… DOMRenderer extraction tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-extracted-domrenderer.js b/testdrive-jsui/static/js/tests/test-extracted-domrenderer.js deleted file mode 100644 index d0a8990a..00000000 --- a/testdrive-jsui/static/js/tests/test-extracted-domrenderer.js +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Extracted DOMRenderer Component - * - * Tests the extracted DOMRenderer component independently from the monolith. - * Verifies that core functionality is preserved after extraction. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Extracted DOMRenderer Component', () => { - - runner.it('should load extracted DOMRenderer component', () => { - // Load the extracted component - delete require.cache[require.resolve('../components/dom-renderer.js')]; - - try { - const module = require('../components/dom-renderer.js'); - runner.expect(module.DOMRenderer).toBeTruthy(); - runner.expect(module.FloatingMenu).toBeTruthy(); - - // Set globals for other tests - global.ExtractedDOMRenderer = module.DOMRenderer; - global.ExtractedFloatingMenu = module.FloatingMenu; - } catch (error) { - throw new Error(`Failed to load extracted DOMRenderer: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - - // Load SectionManager from our extracted core - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - - const renderer = new DOMRenderer(sectionManager, container); - runner.expect(renderer).toBeInstanceOf(DOMRenderer); - runner.expect(renderer.sectionManager).toBe(sectionManager); - runner.expect(renderer.container).toBe(container); - runner.expect(renderer.editingSections).toBeInstanceOf(Set); - }); - - runner.it('should preserve section rendering functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - // This should not throw an error - renderer.renderAllSections(sections); - - // Check that content was rendered - runner.expect(container.innerHTML.length > 100).toBeTruthy(); - runner.expect(container.innerHTML).toContain('Test Heading'); - }); - - runner.it('should preserve findSectionElement functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = renderer.findSectionElement(sectionId); - - runner.expect(element).toBeTruthy(); - runner.expect(element.getAttribute('data-section-id')).toBe(sectionId); - }); - - runner.it('should preserve event tracking functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - // Should have trackEvent method - runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy(); - - // Should be able to track an event - renderer.trackEvent('test-event', { data: 'test' }); - - // Should have getEventStats method - runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy(); - - const stats = renderer.getEventStats(); - runner.expect(typeof stats === 'object').toBeTruthy(); - runner.expect(stats).toHaveProperty('stats'); - runner.expect(stats).toHaveProperty('totalEvents'); - runner.expect(stats).toHaveProperty('recentEvents'); - }); - - runner.it('should preserve editor showing functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - - // showEditor should not throw error - try { - renderer.showEditor(sectionId, 'test content'); - runner.expect(true).toBeTruthy(); // If we get here, no error was thrown - - // Check that editing state was set - runner.expect(renderer.editingSections.has(sectionId)).toBeTruthy(); - } catch (error) { - throw new Error(`showEditor failed: ${error.message}`); - } - }); - - runner.it('should preserve FloatingMenu functionality', () => { - const FloatingMenu = global.ExtractedFloatingMenu; - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const floatingMenu = new FloatingMenu(sectionId, 'text', renderer); - - runner.expect(floatingMenu.sectionId).toBe(sectionId); - runner.expect(floatingMenu.type).toBe('text'); - runner.expect(floatingMenu.renderer).toBe(renderer); - runner.expect(floatingMenu.isVisible).toBeFalsy(); - - // Test show/hide functionality - const content = document.createElement('div'); - content.textContent = 'Test content'; - - floatingMenu.show(content); - runner.expect(floatingMenu.isVisible).toBeTruthy(); - - floatingMenu.hide(); - runner.expect(floatingMenu.isVisible).toBeFalsy(); - }); - - runner.it('should handle section click events', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = renderer.findSectionElement(sectionId); - - // Simulate a click event - const clickEvent = new Event('click', { bubbles: true }); - Object.defineProperty(clickEvent, 'target', { value: element }); - - // Should not throw error - try { - renderer.handleSectionClick(clickEvent); - runner.expect(true).toBeTruthy(); - } catch (error) { - throw new Error(`handleSectionClick failed: ${error.message}`); - } - }); - - // Comparative test - verify extracted component behaves similarly to original - runner.it('should behave similarly to original monolithic component', () => { - // Load both components - const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - const extractedModule = require('../components/dom-renderer.js'); - const sectionModule = require('../core/section-manager.js'); - - const originalSectionManager = new originalModule.SectionManager(); - const extractedSectionManager = new sectionModule.SectionManager(); - - const originalContainer = document.createElement('div'); - originalContainer.innerHTML = '
'; - - const extractedContainer = document.createElement('div'); - extractedContainer.innerHTML = '
'; - - const originalRenderer = new originalModule.DOMRenderer(originalSectionManager, originalContainer); - const extractedRenderer = new extractedModule.DOMRenderer(extractedSectionManager, extractedContainer); - - const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content'; - - // Create sections with both - const originalSections = originalSectionManager.createSectionsFromMarkdown(testMarkdown); - const extractedSections = extractedSectionManager.createSectionsFromMarkdown(testMarkdown); - - // Render with both - originalRenderer.renderAllSections(originalSections); - extractedRenderer.renderAllSections(extractedSections); - - // Should have rendered content - runner.expect(originalContainer.innerHTML.length > 100).toBeTruthy(); - runner.expect(extractedContainer.innerHTML.length > 100).toBeTruthy(); - - // Should have same number of section elements - const originalSectionElements = originalContainer.querySelectorAll('.ui-edit-section'); - const extractedSectionElements = extractedContainer.querySelectorAll('.ui-edit-section'); - - runner.expect(extractedSectionElements.length).toBe(originalSectionElements.length); - - // Should have similar event stats structure - const originalStats = originalRenderer.getEventStats(); - const extractedStats = extractedRenderer.getEventStats(); - - runner.expect(extractedStats).toHaveProperty('stats'); - runner.expect(extractedStats).toHaveProperty('totalEvents'); - runner.expect(extractedStats).toHaveProperty('recentEvents'); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing Extracted DOMRenderer Component'); - runner.run().then(() => { - console.log('โœ… Extracted DOMRenderer tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-extracted-section-manager.js b/testdrive-jsui/static/js/tests/test-extracted-section-manager.js deleted file mode 100644 index 0eb51d01..00000000 --- a/testdrive-jsui/static/js/tests/test-extracted-section-manager.js +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Extracted SectionManager Component - * - * Tests the extracted SectionManager component independently from the monolith. - * Verifies that all functionality is preserved after extraction. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Extracted SectionManager Component', () => { - - runner.it('should load extracted SectionManager component', () => { - // Load the extracted component - delete require.cache[require.resolve('../core/section-manager.js')]; - - try { - const module = require('../core/section-manager.js'); - runner.expect(module.SectionManager).toBeTruthy(); - runner.expect(module.Section).toBeTruthy(); - runner.expect(module.EditState).toBeTruthy(); - runner.expect(module.SectionType).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = module.SectionManager; - global.ExtractedSection = module.Section; - global.ExtractedEditState = module.EditState; - global.ExtractedSectionType = module.SectionType; - } catch (error) { - throw new Error(`Failed to load extracted SectionManager: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const SectionManager = global.ExtractedSectionManager; - - const manager = new SectionManager(); - runner.expect(manager).toBeInstanceOf(SectionManager); - runner.expect(manager.sections).toBeInstanceOf(Map); - runner.expect(manager.listeners).toBeInstanceOf(Map); - }); - - runner.it('should preserve section creation functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`; - const sections = manager.createSectionsFromMarkdown(testMarkdown); - - runner.expect(Array.isArray(sections)).toBeTruthy(); - runner.expect(sections.length).toBe(2); - runner.expect(sections[0].currentMarkdown).toContain('Heading 1'); - runner.expect(sections[1].currentMarkdown).toContain('Heading 2'); - }); - - runner.it('should preserve section editing functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Test\nContent'); - const sectionId = sections[0].id; - - // Test start editing - const content = manager.startEditing(sectionId); - runner.expect(content).toContain('Test'); - - const section = manager.sections.get(sectionId); - runner.expect(section.isEditing()).toBeTruthy(); - - // Test stop editing - section.stopEditing(); - runner.expect(section.isEditing()).toBeFalsy(); - }); - - runner.it('should preserve event system functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - let eventFired = false; - let eventData = null; - - manager.on('test-event', (data) => { - eventFired = true; - eventData = data; - }); - - manager.emit('test-event', { test: 'data' }); - - runner.expect(eventFired).toBeTruthy(); - runner.expect(eventData).toEqual({ test: 'data' }); - }); - - runner.it('should preserve document status functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - manager.createSectionsFromMarkdown('# Test\nContent'); - const status = manager.getDocumentStatus(); - - runner.expect(status).toHaveProperty('totalSections'); - runner.expect(status).toHaveProperty('editingSections'); - runner.expect(status.totalSections).toBe(1); - }); - - runner.it('should preserve getAllSections functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const testMarkdown = '# One\nContent\n\n# Two\nMore content'; - manager.createSectionsFromMarkdown(testMarkdown); - - const allSections = manager.getAllSections(); - runner.expect(Array.isArray(allSections)).toBeTruthy(); - runner.expect(allSections.length).toBe(2); - }); - - runner.it('should preserve section splitting functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Original\nContent'); - const sectionId = sections[0].id; - - const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2'; - const newSections = manager.handleSectionSplit(sectionId, newContent); - - runner.expect(Array.isArray(newSections)).toBeTruthy(); - runner.expect(newSections.length).toBe(2); - runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed - }); - - runner.it('should preserve Section class functionality', () => { - const Section = global.ExtractedSection; - const EditState = global.ExtractedEditState; - - const section = new Section('test-id', '# Test Content', 'heading'); - - runner.expect(section.id).toBe('test-id'); - runner.expect(section.currentMarkdown).toBe('# Test Content'); - runner.expect(section.type).toBe('heading'); - runner.expect(section.state).toBe(EditState.ORIGINAL); - }); - - runner.it('should preserve Section ID generation', () => { - const Section = global.ExtractedSection; - - const id1 = Section.generateId('# Test Heading', 0); - const id2 = Section.generateId('# Different Heading', 1); - - runner.expect(typeof id1 === 'string').toBeTruthy(); - runner.expect(typeof id2 === 'string').toBeTruthy(); - runner.expect(id1).toContain('section-'); - runner.expect(id2).toContain('section-'); - runner.expect(id1 !== id2).toBeTruthy(); // Should be unique - }); - - runner.it('should preserve Section type detection', () => { - const Section = global.ExtractedSection; - const SectionType = global.ExtractedSectionType; - - runner.expect(Section.detectType('# Heading')).toBe(SectionType.HEADING); - runner.expect(Section.detectType('![Image](url)')).toBe(SectionType.IMAGE); - runner.expect(Section.detectType('```code```')).toBe(SectionType.CODE); - runner.expect(Section.detectType('Regular paragraph')).toBe(SectionType.PARAGRAPH); - }); - - // Comparative test - verify extracted component behaves identically to original - runner.it('should behave identically to original monolithic component', () => { - // Load both components - const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - const extractedModule = require('../core/section-manager.js'); - - const originalManager = new originalModule.SectionManager(); - const extractedManager = new extractedModule.SectionManager(); - - const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content'; - - // Debug: Check what each component produces - console.log('Creating sections with original component...'); - const originalSections = originalManager.createSectionsFromMarkdown(testMarkdown); - console.log(`Original produced ${originalSections.length} sections`); - - console.log('Creating sections with extracted component...'); - const extractedSections = extractedManager.createSectionsFromMarkdown(testMarkdown); - console.log(`Extracted produced ${extractedSections.length} sections`); - - if (originalSections.length > 0) { - console.log('Original first section:', originalSections[0].currentMarkdown); - } - if (extractedSections.length > 0) { - console.log('Extracted first section:', extractedSections[0].currentMarkdown); - } - - // Should have same number of sections - runner.expect(extractedSections.length).toBe(originalSections.length); - - // Should have same content - for (let i = 0; i < originalSections.length; i++) { - runner.expect(extractedSections[i].currentMarkdown).toBe(originalSections[i].currentMarkdown); - runner.expect(extractedSections[i].type).toBe(originalSections[i].type); - } - - // Should have same document status structure - const originalStatus = originalManager.getDocumentStatus(); - const extractedStatus = extractedManager.getDocumentStatus(); - - console.log('Original status:', originalStatus); - console.log('Extracted status:', extractedStatus); - - runner.expect(extractedStatus.totalSections).toBe(originalStatus.totalSections); - runner.expect(extractedStatus.editingSections).toBe(originalStatus.editingSections); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing Extracted SectionManager Component'); - runner.run().then(() => { - console.log('โœ… Extracted SectionManager tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-full-integration.js b/testdrive-jsui/static/js/tests/test-full-integration.js deleted file mode 100644 index 3edb0ced..00000000 --- a/testdrive-jsui/static/js/tests/test-full-integration.js +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env node - -/** - * Full Integration Test - * - * Tests that all extracted components (SectionManager, DOMRenderer, - * DebugPanel, DocumentControls) work together as a complete system. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Full Component Integration Tests', () => { - - runner.it('should load all extracted components', () => { - try { - // Load all extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); - - runner.expect(sectionModule.SectionManager).toBeTruthy(); - runner.expect(domModule.DOMRenderer).toBeTruthy(); - runner.expect(debugModule.DebugPanel).toBeTruthy(); - runner.expect(controlsModule.DocumentControls).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = sectionModule.SectionManager; - global.ExtractedDOMRenderer = domModule.DOMRenderer; - global.ExtractedDebugPanel = debugModule.DebugPanel; - global.ExtractedDocumentControls = controlsModule.DocumentControls; - - } catch (error) { - throw new Error(`Failed to load extracted components: ${error.message}`); - } - }); - - runner.it('should support complete document editing workflow with all components', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Setup DOM container - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - // Create all components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // Setup document controls - documentControls.create(); - - // Wire up event handlers for debugging - sectionManager.on('sections-created', (data) => { - debugPanel.addMessage(`Created ${data.count} sections`, 'INFO'); - }); - - sectionManager.on('edit-started', (data) => { - debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG'); - }); - - // Test workflow: Create document - const testMarkdown = `# Document Title -Introduction paragraph with some content. - -## Section A -Content for section A with details. - -![Test Image](https://example.com/test.jpg) - -### Subsection A.1 -More detailed content here.`; - - // Create sections - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - runner.expect(sections.length).toBe(4); - - // Render sections - domRenderer.renderAllSections(sections); - const renderedElements = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedElements.length).toBe(sections.length); - - // Test editing workflow - const firstSection = sections[0]; - sectionManager.startEditing(firstSection.id); - runner.expect(firstSection.isEditing()).toBeTruthy(); - - // Check debug messages were created - runner.expect(debugPanel.getMessageCount()).toBe(2); // sections-created + edit-started - - // Test document controls functionality - const controlPanel = documentControls.getControlPanel(); - runner.expect(controlPanel).toBeTruthy(); - runner.expect(document.getElementById('save-document')).toBeTruthy(); - runner.expect(document.getElementById('toggle-debug')).toBeTruthy(); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should support debug panel integration with document controls', () => { - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Create components - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // Setup document controls - documentControls.create(); - - // Setup debug panel toggle handler - const handlers = { - 'toggle-debug': () => debugPanel.toggle() - }; - documentControls.setEventHandlers(handlers); - - // Test debug toggle functionality - const debugButton = documentControls.getButton('toggle-debug'); - runner.expect(debugButton).toBeTruthy(); - - // Add some debug messages - debugPanel.addMessage('Test message 1', 'INFO'); - debugPanel.addMessage('Test message 2', 'ERROR'); - - // Simulate button click to show debug panel - debugButton.click(); - runner.expect(debugPanel.isActive).toBeTruthy(); - - // Simulate button click to hide debug panel - debugButton.click(); - runner.expect(debugPanel.isActive).toBeFalsy(); - - // Cleanup - documentControls.destroy(); - }); - - runner.it('should support event-driven communication between all components', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Setup container - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - // Create components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - documentControls.create(); - - // Setup comprehensive event handling - let eventLog = []; - - sectionManager.on('sections-created', (data) => { - eventLog.push(`sections-created: ${data.count} sections`); - debugPanel.addMessage(`Sections created: ${data.count}`, 'INFO'); - }); - - sectionManager.on('edit-started', (data) => { - eventLog.push(`edit-started: ${data.sectionId}`); - debugPanel.addMessage(`Edit started: ${data.sectionId}`, 'DEBUG'); - }); - - sectionManager.on('changes-accepted', (data) => { - eventLog.push(`changes-accepted: ${data.sectionId}`); - debugPanel.addMessage(`Changes accepted: ${data.sectionId}`, 'SUCCESS'); - }); - - // Test complete workflow - const testMarkdown = '# Test\nContent for testing'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - // Start editing - sectionManager.startEditing(sections[0].id); - sectionManager.updateContent(sections[0].id, '# Updated Test\nUpdated content'); - sectionManager.acceptChanges(sections[0].id); - - // Verify events were logged - runner.expect(eventLog.length).toBe(3); - runner.expect(eventLog[0]).toContain('sections-created'); - runner.expect(eventLog[1]).toContain('edit-started'); - runner.expect(eventLog[2]).toContain('changes-accepted'); - - // Verify debug messages were created - runner.expect(debugPanel.getMessageCount()).toBe(3); - - // Test document controls status update - const status = sectionManager.getDocumentStatus(); - documentControls.updateStatus(status); - runner.expect(documentControls.lastStatus).toBeTruthy(); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should handle error scenarios gracefully across components', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Test component creation without proper DOM setup - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // These should not throw errors - try { - debugPanel.toggle(); // No DOM elements - debugPanel.update(); // No DOM elements - documentControls.show(); // No control panel created yet - documentControls.hide(); // No control panel created yet - - runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown - } catch (error) { - throw new Error(`Components should handle missing DOM gracefully: ${error.message}`); - } - - // Test section manager with invalid input - const sectionManager = new SectionManager(); - const sections = sectionManager.createSectionsFromMarkdown(''); - runner.expect(sections.length).toBe(0); - - // Test DOM renderer with invalid container - try { - const invalidRenderer = new DOMRenderer(sectionManager, null); - runner.expect(invalidRenderer.container).toBeFalsy(); - } catch (error) { - // This is acceptable - constructor might validate input - runner.expect(typeof error.message === 'string').toBeTruthy(); - } - }); - - runner.it('should support scalable architecture with component lifecycle', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Test multiple instances - const sectionManager1 = new SectionManager(); - const sectionManager2 = new SectionManager(); - const debugPanel1 = new DebugPanel(); - const debugPanel2 = new DebugPanel(); - - // Each should be independent - debugPanel1.addMessage('Message from panel 1', 'INFO'); - debugPanel2.addMessage('Message from panel 2', 'ERROR'); - - runner.expect(debugPanel1.getMessageCount()).toBe(1); - runner.expect(debugPanel2.getMessageCount()).toBe(1); - - // Test section managers are independent - const sections1 = sectionManager1.createSectionsFromMarkdown('# Document 1'); - const sections2 = sectionManager2.createSectionsFromMarkdown('# Document 2'); - - runner.expect(sections1.length).toBe(1); - runner.expect(sections2.length).toBe(1); - runner.expect(sections1[0]).toBeTruthy(); - runner.expect(sections2[0]).toBeTruthy(); - - // IDs should be different (each section gets unique ID) - const id1 = sections1[0].id; - const id2 = sections2[0].id; - runner.expect(id1 !== id2).toBeTruthy(); - - // Test document controls lifecycle - const controls1 = new DocumentControls(); - const controls2 = new DocumentControls(); - - controls1.create(); - runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy(); - - controls2.create(); // Should replace the first one - runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy(); - - controls2.destroy(); - runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy(); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running Full Component Integration Tests'); - runner.run().then(() => { - console.log('โœ… Full integration tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-navigator-demo.html b/testdrive-jsui/static/js/tests/test-navigator-demo.html deleted file mode 100644 index 020178b1..00000000 --- a/testdrive-jsui/static/js/tests/test-navigator-demo.html +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - DocumentNavigator Live Demo - - - -
-

๐Ÿ“‹ DocumentNavigator Live Demo

-

This page demonstrates the Substack-style floating navigation widget in action.

-

Look for the hamburger menu (โ˜ฐ) on the left side!

- -
- Features to test:
- โ€ข Click the hamburger menu to expand navigation
- โ€ข Click any heading in the navigator to jump to it
- โ€ข Scroll and watch the current section highlight
- โ€ข Try keyboard shortcuts (Enter/Space to toggle, Escape to close)
- โ€ข Resize window to test responsive behavior -
-
- -
-

1. Introduction to MarkiTect

-
-

MarkiTect is an advanced markdown processing engine that provides sophisticated document management capabilities. This demo showcases the DocumentNavigator widget, which provides Substack-style navigation for long-form documents.

- -

The navigator automatically extracts headings from your content and builds a hierarchical table of contents that floats elegantly on the side of your document.

-
- -

1.1 Core Features

-
-

The DocumentNavigator widget includes numerous advanced features designed for optimal user experience:

- -
    -
  • Automatic Heading Detection: Scans document for H1, H2, H3 elements
  • -
  • Hierarchical Structure: Maintains proper heading hierarchy with indentation
  • -
  • Scroll Spy: Highlights current section as you scroll
  • -
  • Smooth Navigation: Animated scrolling to clicked sections
  • -
  • Responsive Design: Auto-hides on mobile devices
  • -
-
- -

1.1.1 Responsive Behavior

-
-

The navigator intelligently adapts to different screen sizes. On desktop computers, it remains visible as a floating panel. On mobile devices, it automatically hides to preserve screen real estate for content.

- -

Try resizing your browser window to see this behavior in action. The navigator will disappear when the viewport becomes narrow (under 768px wide).

-
- -

1.1.2 Accessibility Features

-
-

The DocumentNavigator is built with accessibility in mind:

- -
    -
  • Full keyboard navigation support
  • -
  • ARIA labels and proper semantic markup
  • -
  • Screen reader compatibility
  • -
  • High contrast hover states
  • -
  • Focus management
  • -
-
- -

1.2 Implementation Details

-
-

The DocumentNavigator is implemented as a modular ES6 class that extends our base UIWidget class. This follows the planned plugin architecture for MarkiTect widgets.

- -

Key implementation highlights include:

- -
    -
  • extractHeadings() - Scans DOM for heading elements
  • -
  • buildNavigationTree() - Creates hierarchical structure
  • -
  • handleScroll() - Manages scroll spy functionality
  • -
  • navigateToHeading() - Handles smooth scrolling
  • -
-
- -

2. Widget Architecture

-
-

The DocumentNavigator follows a clean architectural pattern that separates concerns and provides maximum flexibility for customization and extension.

- -

The widget is designed as part of a larger plugin ecosystem that will allow developers to create custom UI components that can be loaded dynamically and configured independently.

-
- -

2.1 Base Class Hierarchy

-
-

Our widget system is built on a foundation of base classes that provide common functionality:

- -
    -
  • Widget: Core functionality (events, state, lifecycle)
  • -
  • UIWidget: DOM manipulation and visual behavior
  • -
  • InteractiveWidget: Event handling and user interaction
  • -
- -

DocumentNavigator extends UIWidget directly since it doesn't require complex interaction handling beyond basic click and keyboard events.

-
- -

2.1.1 Event System

-
-

The widget uses a custom event system built on the native EventTarget API. This allows for clean separation of concerns and easy integration with other components.

- -

Key events emitted by DocumentNavigator:

- -
    -
  • rendered - Widget has been rendered to DOM
  • -
  • navigate - User navigated to a heading
  • -
  • toggle - Widget was expanded or collapsed
  • -
  • theme-changed - Theme was changed
  • -
  • destroyed - Widget was destroyed
  • -
-
- -

2.1.2 State Management

-
-

State management is handled through a simple Map-based system that provides reactive updates and event emission when state changes occur.

- -

This approach is lightweight but powerful enough for most widget use cases while remaining debuggable and predictable.

-
- -

2.2 Plugin System Integration

-
-

While the current implementation works standalone, it's designed to integrate seamlessly with our planned plugin system. The plugin definition includes:

- -
    -
  • Metadata and versioning information
  • -
  • Dependency declarations
  • -
  • Default configuration options
  • -
  • Lifecycle hooks
  • -
  • Theme variants
  • -
  • Development helpers
  • -
-
- -

3. Usage Examples

-
-

The DocumentNavigator can be used in several ways, from simple instantiation to advanced configuration with custom themes and behavior.

-
- -

3.1 Basic Usage

-
-

The simplest way to use DocumentNavigator is with default settings:

- -
const navigator = new DocumentNavigator();
-await navigator.initialize();
-await navigator.render();
- -

This creates a navigator with default settings that will scan the entire document for headings and display them in a collapsible panel on the left side.

-
- -

3.2 Advanced Configuration

-
-

For more control, you can specify detailed configuration options:

- -
const navigator = new DocumentNavigator({
-    position: 'right',
-    collapsed: false,
-    theme: 'dark',
-    maxHeadingLevel: 4,
-    enableScrollSpy: true,
-    smoothScroll: true
-});
- -

This creates a navigator on the right side that starts expanded, includes H4 headings, and uses the dark theme.

-
- -

3.2.1 Custom Theming

-
-

The navigator supports multiple built-in themes and can be extended with custom themes. The theming system integrates with MarkiTect's document themes for consistent styling.

- -

Available themes include default, dark, and minimal, each optimized for different use cases and aesthetics.

-
- -

4. Testing and Quality

-
-

The DocumentNavigator implementation follows Test-Driven Development (TDD) methodology with comprehensive test coverage ensuring reliability and maintainability.

-
- -

4.1 Test Coverage

-
-

Our test suite covers all major functionality:

- -
    -
  • Widget instantiation and configuration
  • -
  • DOM rendering and element creation
  • -
  • Heading extraction and hierarchy building
  • -
  • Navigation and smooth scrolling
  • -
  • Expand/collapse animations
  • -
  • Scroll spy functionality
  • -
  • Responsive behavior
  • -
  • Keyboard navigation
  • -
  • Event emission
  • -
  • Edge cases and error handling
  • -
-
- -

4.2 Performance Considerations

-
-

The navigator is optimized for performance with several key strategies:

- -
    -
  • Throttled Scroll Events: Scroll spy updates are throttled to 100ms intervals
  • -
  • Efficient DOM Queries: Heading extraction is done once and cached
  • -
  • Conditional Rendering: Navigator only renders if minimum heading count is met
  • -
  • Memory Management: Proper cleanup prevents memory leaks
  • -
  • Responsive Loading: Navigator automatically hides on mobile to save resources
  • -
-
- -

5. Conclusion

-
-

The DocumentNavigator widget successfully brings Substack-style navigation to MarkiTect documents. It provides an intuitive, accessible, and performant way for users to navigate long-form content.

- -

The implementation demonstrates the power of our widget architecture approach, with clean separation of concerns, comprehensive testing, and excellent extensibility for future enhancements.

- -

Scroll back to the top and try the navigation features! The hamburger menu should be visible on the left side of your screen.

-
-
- - - - - \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-real-user-functionality.js b/testdrive-jsui/static/js/tests/test-real-user-functionality.js deleted file mode 100644 index 3d7fddef..00000000 --- a/testdrive-jsui/static/js/tests/test-real-user-functionality.js +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env node - -/** - * Real User Functionality Tests - * - * This test file validates the actual functionality that users experience, - * not just internal API calls. It tests the complete user workflow. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Real User Functionality Tests', () => { - - runner.it('should allow users to edit content and see changes in DOM', () => { - // Load all extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - const { DebugPanel } = debugModule; - const { DocumentControls } = controlsModule; - - // Setup DOM container - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - // Create components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // Setup document controls - documentControls.create(); - - // Create sections from test markdown - const testMarkdown = `# Original Title\nOriginal content that should be editable.`; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const firstSection = sections[0]; - const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - - // Verify original content is rendered - runner.expect(sectionElement.innerHTML).toContain('Original Title'); - - // Simulate user clicking on section - const clickEvent = new Event('click', { bubbles: true }); - sectionElement.dispatchEvent(clickEvent); - - // Verify editing state is active - runner.expect(firstSection.isEditing()).toBeTruthy(); - - // Find the floating menu and edit controls - const floatingMenu = document.querySelector('.ui-edit-floating-menu'); - runner.expect(floatingMenu).toBeTruthy(); - - const textarea = floatingMenu.querySelector('textarea'); - const acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept')); - - runner.expect(textarea).toBeTruthy(); - runner.expect(acceptButton).toBeTruthy(); - - // Simulate user editing content - const newContent = '# Updated Title\nCompletely new content added by user.'; - textarea.value = newContent; - - // Simulate user clicking accept - acceptButton.click(); - - // Verify section is no longer editing - runner.expect(firstSection.isEditing()).toBeFalsy(); - - // Verify floating menu is gone - const menuAfterAccept = document.querySelector('.ui-edit-floating-menu'); - runner.expect(menuAfterAccept).toBeFalsy(); - - // CRITICAL TEST: Verify DOM was actually updated with new content - const updatedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(updatedElement.innerHTML).toContain('Updated Title'); - runner.expect(updatedElement.innerHTML).toContain('Completely new content'); - runner.expect(updatedElement.innerHTML).not.toContain('Original Title'); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should allow users to reset all changes', () => { - // Setup similar to above - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const controlsModule = require('../components/document-controls.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - const { DocumentControls } = controlsModule; - - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const documentControls = new DocumentControls(); - - documentControls.create(); - - // Create and modify content - const testMarkdown = `# Test Section\nOriginal content for reset test.`; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const firstSection = sections[0]; - - // Make changes to the section - sectionManager.startEditing(firstSection.id); - sectionManager.updateContent(firstSection.id, '# Modified Title\nModified content.'); - sectionManager.acceptChanges(firstSection.id); - - // Verify changes are applied - let sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(sectionElement.innerHTML).toContain('Modified Title'); - runner.expect(firstSection.hasChanges()).toBeTruthy(); - - // Test reset functionality - const resetButton = documentControls.getButton('reset-all'); - runner.expect(resetButton).toBeTruthy(); - - // Click reset button - resetButton.click(); - - // Verify content is reset - sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(sectionElement.innerHTML).toContain('Test Section'); - runner.expect(sectionElement.innerHTML).not.toContain('Modified Title'); - runner.expect(firstSection.hasChanges()).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should handle cancel operations correctly', () => { - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = `# Cancel Test\nContent that should remain unchanged.`; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const firstSection = sections[0]; - const originalContent = firstSection.currentMarkdown; - - // Start editing - const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - sectionElement.click(); - - // Make changes but cancel them - const floatingMenu = document.querySelector('.ui-edit-floating-menu'); - const textarea = floatingMenu.querySelector('textarea'); - const cancelButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Cancel')); - - textarea.value = '# This should be cancelled\nThis content should not appear.'; - cancelButton.click(); - - // Verify content is unchanged - const unchangedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(unchangedElement.innerHTML).toContain('Cancel Test'); - runner.expect(unchangedElement.innerHTML).not.toContain('This should be cancelled'); - runner.expect(firstSection.currentMarkdown).toBe(originalContent); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should validate the complete editing workflow', () => { - // This test validates the entire user experience end-to-end - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - const { DebugPanel } = debugModule; - const { DocumentControls } = controlsModule; - - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - documentControls.create(); - - // Multi-section document - const testMarkdown = `# Document Title -Introduction paragraph. - -## Section A -Content for section A. - -## Section B -Content for section B.`; - - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - // Verify all sections are rendered - const renderedSections = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedSections.length).toBe(sections.length); - - // Test editing multiple sections - const firstSection = sections[0]; - const secondSection = sections[2]; // Section A - - // Edit first section - renderedSections[0].click(); - let floatingMenu = document.querySelector('.ui-edit-floating-menu'); - let textarea = floatingMenu.querySelector('textarea'); - let acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept')); - - textarea.value = '# Updated Document Title\nUpdated introduction.'; - acceptButton.click(); - - // Edit second section - renderedSections[2].click(); - floatingMenu = document.querySelector('.ui-edit-floating-menu'); - textarea = floatingMenu.querySelector('textarea'); - acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept')); - - textarea.value = '## Updated Section A\nCompletely new content for section A.'; - acceptButton.click(); - - // Verify both sections were updated - const updatedSections = container.querySelectorAll('.ui-edit-section'); - runner.expect(updatedSections[0].innerHTML).toContain('Updated Document Title'); - runner.expect(updatedSections[2].innerHTML).toContain('Updated Section A'); - - // Test reset restores all sections - const resetButton = documentControls.getButton('reset-all'); - resetButton.click(); - - const resetSections = container.querySelectorAll('.ui-edit-section'); - runner.expect(resetSections[0].innerHTML).toContain('Document Title'); - runner.expect(resetSections[0].innerHTML).not.toContain('Updated Document Title'); - runner.expect(resetSections[2].innerHTML).toContain('Section A'); - runner.expect(resetSections[2].innerHTML).not.toContain('Updated Section A'); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running Real User Functionality Tests'); - runner.run().then(() => { - console.log('โœ… Real user functionality tests completed'); - console.log('These tests validate what users actually experience, not just internal APIs'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test-section-manager-extraction.js b/testdrive-jsui/static/js/tests/test-section-manager-extraction.js deleted file mode 100644 index 1eecce5d..00000000 --- a/testdrive-jsui/static/js/tests/test-section-manager-extraction.js +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for SectionManager Component Extraction - * - * Tests the extraction of SectionManager from the monolithic editor.js - * Ensures all functionality is preserved during refactoring. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// First, let's define what the SectionManager API should look like -const EXPECTED_SECTION_MANAGER_API = [ - 'constructor', - 'createSectionsFromMarkdown', - 'startEditing', - 'stopEditing', - 'getAllSections', - 'sections', // Map property, not method - 'getDocumentStatus', - 'getDocumentMarkdown', - 'on', // event system - 'emit', // event system - 'handleSectionSplit', - 'updateContent', - 'acceptChanges', - 'cancelChanges', - 'resetSection' -]; - -runner.describe('SectionManager Component Extraction', () => { - - runner.it('should define expected API methods', () => { - // This test defines what we expect from the extracted SectionManager - const expectedMethods = EXPECTED_SECTION_MANAGER_API; - runner.expect(expectedMethods.length).toBe(15); - runner.expect(expectedMethods).toContain('createSectionsFromMarkdown'); - runner.expect(expectedMethods).toContain('startEditing'); - runner.expect(expectedMethods).toContain('stopEditing'); - }); - - runner.it('should extract from monolithic editor.js', () => { - // Load the monolithic editor.js to extract SectionManager - delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')]; - - try { - const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - runner.expect(editorModule.SectionManager).toBeTruthy(); - // Set global for other tests - global.SectionManager = editorModule.SectionManager; - global.Section = editorModule.Section; - global.EditState = editorModule.EditState; - } catch (error) { - throw new Error(`Failed to load monolithic editor.js: ${error.message}`); - } - }); - - runner.it('should preserve SectionManager constructor functionality', () => { - const SectionManager = global.SectionManager; - - const manager = new SectionManager(); - runner.expect(manager).toBeInstanceOf(SectionManager); - runner.expect(manager.sections).toBeInstanceOf(Map); - }); - - runner.it('should preserve createSectionsFromMarkdown functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`; - const sections = manager.createSectionsFromMarkdown(testMarkdown); - - runner.expect(Array.isArray(sections)).toBeTruthy(); - runner.expect(sections.length).toBe(2); - runner.expect(sections[0].currentMarkdown).toContain('Heading 1'); - runner.expect(sections[1].currentMarkdown).toContain('Heading 2'); - }); - - runner.it('should preserve section editing state management', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Test\nContent'); - const sectionId = sections[0].id; - - // Test start editing - runner.expect(manager.startEditing(sectionId)).toBeTruthy(); - const section = manager.sections.get(sectionId); - runner.expect(section.isEditing()).toBeTruthy(); - - // Test stop editing - section.stopEditing(); - runner.expect(section.isEditing()).toBeFalsy(); - }); - - runner.it('should preserve event system functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - let eventFired = false; - let eventData = null; - - manager.on('test-event', (data) => { - eventFired = true; - eventData = data; - }); - - manager.emit('test-event', { test: 'data' }); - - runner.expect(eventFired).toBeTruthy(); - runner.expect(eventData).toEqual({ test: 'data' }); - }); - - runner.it('should preserve document status functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - manager.createSectionsFromMarkdown('# Test\nContent'); - const status = manager.getDocumentStatus(); - - runner.expect(status).toHaveProperty('totalSections'); - runner.expect(status).toHaveProperty('editingSections'); - runner.expect(status.totalSections).toBe(1); - }); - - runner.it('should preserve getAllSections functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const testMarkdown = '# One\nContent\n\n# Two\nMore content'; - manager.createSectionsFromMarkdown(testMarkdown); - - const allSections = manager.getAllSections(); - runner.expect(Array.isArray(allSections)).toBeTruthy(); - runner.expect(allSections.length).toBe(2); - }); - - runner.it('should preserve section splitting functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Original\nContent'); - const sectionId = sections[0].id; - - const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2'; - const newSections = manager.handleSectionSplit(sectionId, newContent); - - runner.expect(Array.isArray(newSections)).toBeTruthy(); - runner.expect(newSections.length).toBe(2); - runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed - }); -}); - -// Export API tests for use during extraction -const SECTION_MANAGER_API_TESTS = [ - (SectionManager) => { - const manager = new SectionManager(); - if (!manager.sections || !(manager.sections instanceof Map)) { - throw new Error('sections property missing or not a Map'); - } - }, - (SectionManager) => { - const manager = new SectionManager(); - if (typeof manager.createSectionsFromMarkdown !== 'function') { - throw new Error('createSectionsFromMarkdown method missing'); - } - }, - (SectionManager) => { - const manager = new SectionManager(); - if (typeof manager.startEditing !== 'function') { - throw new Error('startEditing method missing'); - } - }, - (SectionManager) => { - const manager = new SectionManager(); - if (typeof manager.stopEditing !== 'function') { - throw new Error('stopEditing method missing'); - } - } -]; - -module.exports = { - runner, - EXPECTED_SECTION_MANAGER_API, - SECTION_MANAGER_API_TESTS -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing SectionManager Component Extraction'); - runner.run().then(() => { - console.log('โœ… SectionManager extraction tests completed'); - }); -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test.md b/testdrive-jsui/static/js/tests/test.md deleted file mode 100644 index 239c58bf..00000000 --- a/testdrive-jsui/static/js/tests/test.md +++ /dev/null @@ -1,6 +0,0 @@ -# Test Document - -This is a test document to check if UI controls appear in edit mode. - -## Section 1 -Some content here. diff --git a/testdrive-jsui/static/js/tests/test_edit.html b/testdrive-jsui/static/js/tests/test_edit.html deleted file mode 100644 index 813b65bf..00000000 --- a/testdrive-jsui/static/js/tests/test_edit.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - Test Document - - - - - - - - - - - -
-

Test Document

-

This is a test document to check if UI controls appear in edit mode.

-

Section 1

-

Some content here.

-
-

-- html from markdown by MarkiTect on 2025-11-11 23:42:23 by worsch

-
- - - - - - - - - - - - - - - \ No newline at end of file