feat: consolidate testdrive-jsui to capabilities and implement plugin self-declaration
## Major Changes - Moved all testdrive-jsui assets from root to capabilities/testdrive-jsui/ - Consolidated directory structure: js/, static/css/, static/images/, static/templates/ - Implemented plugin self-declaration (get_plugin_source_dir, get_asset_paths) - Removed hardcoded plugin discovery from rendering.py - Updated all asset paths to be relative to capability root ## Architecture Improvements - Single source of truth for all testdrive-jsui assets - Plugin declares its own location (no hardcoded paths) - Generic plugin discovery using hasattr check - Clean separation: all JS in .js files, no code mixing - Standalone capability ready for independent use ## Files Changed - markitect/plugins/testdrive_jsui.py: Added self-declaration methods - markitect/plugins/rendering.py: Removed hardcoded discovery - capabilities/testdrive-jsui/README.md: Added standalone usage documentation - Moved 17 asset files to consolidated structure - Deleted obsolete /testdrive-jsui/ root directory ## Testing - All 17 assets verified and working - Tested via CLI: markitect md-render --engine testdrive-jsui - Full document rendering successful Prepares testdrive-jsui to become a git submodule with proper dependency management. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 <repo-url> 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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
<script id="markitect-config" type="application/json">
|
||||
{
|
||||
"markdownContent": "# Document content...",
|
||||
"mode": "edit",
|
||||
"theme": "github",
|
||||
...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 = '<div style="color: #6c757d; font-style: italic; padding: 12px;">No debug messages yet. Click sections to generate debug output.</div>';
|
||||
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 `
|
||||
<div style="margin-bottom: 6px; padding: 4px; border-left: 3px solid ${categoryColor}; background: white; border-radius: 2px;">
|
||||
<span style="color: #6c757d; font-size: 11px;">[${msg.timestamp}]</span>
|
||||
<span style="color: ${categoryColor}; font-weight: bold;">${msg.category}:</span>
|
||||
<span style="color: #333;">${msg.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
debugContainer.innerHTML = `
|
||||
<div style="margin-bottom: 8px; padding: 6px; background: #e9ecef; border-radius: 4px; font-weight: bold; color: #495057;">
|
||||
Debug Messages (${this.messages.length} total, showing last ${recentMessages.length})
|
||||
<button id="debug-clear-btn" style="float: right; background: #dc3545; color: white; border: none; padding: 2px 6px; border-radius: 2px; font-size: 11px; cursor: pointer;">Clear</button>
|
||||
</div>
|
||||
<div style="max-height: 250px; overflow-y: auto;">
|
||||
${messagesHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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('<!DOCTYPE html><html><body></body></html>', {
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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;
|
||||
@@ -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 = '<div id="markdown-content"></div>';
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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.
|
||||
|
||||

|
||||
|
||||
\`\`\`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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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, '');
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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 = '<div id="markdown-content"></div>';
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocumentNavigator TDD Test Runner</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.test-header {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-output {
|
||||
background: #1a1a1a;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', monospace;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 1rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
.run-button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.run-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.run-button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.running {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.status.passed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.failed {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-header">
|
||||
<h1>📋 DocumentNavigator Widget TDD Test Suite</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<strong>Test Coverage:</strong>
|
||||
<ul>
|
||||
<li>✅ Widget class structure and inheritance</li>
|
||||
<li>✅ Configuration and initialization</li>
|
||||
<li>✅ DOM rendering and UI elements</li>
|
||||
<li>✅ Heading extraction and hierarchy building</li>
|
||||
<li>✅ Navigation functionality and smooth scrolling</li>
|
||||
<li>✅ Expand/collapse behavior</li>
|
||||
<li>✅ Scroll spy and active section detection</li>
|
||||
<li>✅ Responsive behavior and auto-hide</li>
|
||||
<li>✅ Keyboard navigation support</li>
|
||||
<li>✅ Event emission and user interaction</li>
|
||||
<li>✅ Edge cases and error handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button id="runTests" class="run-button">🧪 Run TDD Test Suite</button>
|
||||
|
||||
<div id="status" class="status" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div id="testOutput" class="test-output" style="display: none;"></div>
|
||||
|
||||
<script type="module">
|
||||
const runButton = document.getElementById('runTests');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const outputDiv = document.getElementById('testOutput');
|
||||
|
||||
// Capture console output
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
let capturedOutput = '';
|
||||
|
||||
function captureConsole() {
|
||||
capturedOutput = '';
|
||||
|
||||
console.log = (...args) => {
|
||||
capturedOutput += args.join(' ') + '\n';
|
||||
originalConsoleLog(...args);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
capturedOutput += 'ERROR: ' + args.join(' ') + '\n';
|
||||
originalConsoleError(...args);
|
||||
};
|
||||
}
|
||||
|
||||
function restoreConsole() {
|
||||
console.log = originalConsoleLog;
|
||||
console.error = originalConsoleError;
|
||||
}
|
||||
|
||||
function updateStatus(message, type) {
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.className = `status ${type}`;
|
||||
statusDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showOutput() {
|
||||
outputDiv.textContent = capturedOutput;
|
||||
outputDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
runButton.addEventListener('click', async () => {
|
||||
runButton.disabled = true;
|
||||
updateStatus('🧪 Running tests...', 'running');
|
||||
|
||||
captureConsole();
|
||||
|
||||
try {
|
||||
// Import and run tests
|
||||
const { runner } = await import('./test-document-navigator.js');
|
||||
|
||||
console.log('Starting DocumentNavigator TDD Test Suite...\n');
|
||||
console.log('Note: Tests are expected to FAIL initially (Red phase of TDD)');
|
||||
console.log('We will implement functionality to make them pass (Green phase).\n');
|
||||
|
||||
await runner.run();
|
||||
|
||||
if (runner.results.failed === 0) {
|
||||
updateStatus(`🎉 All ${runner.results.total} tests passed!`, 'passed');
|
||||
} else {
|
||||
updateStatus(`❌ ${runner.results.failed} of ${runner.results.total} tests failed (Expected in TDD Red phase)`, 'failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test execution failed:', error);
|
||||
updateStatus('💥 Test execution failed - this is expected in TDD Red phase', 'failed');
|
||||
} finally {
|
||||
restoreConsole();
|
||||
showOutput();
|
||||
runButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-run tests on page load for development
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DocumentNavigator TDD Test Runner loaded');
|
||||
console.log('Ready to run tests - click the button above');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Test content for heading extraction tests -->
|
||||
<div style="display: none;" id="test-content">
|
||||
<h1>Test Chapter 1</h1>
|
||||
<p>Sample content for testing heading extraction.</p>
|
||||
<h2>Section 1.1</h2>
|
||||
<h3>Subsection 1.1.1</h3>
|
||||
<p>More sample content.</p>
|
||||
<h2>Section 1.2</h2>
|
||||
<h1>Test Chapter 2</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 = `
|
||||
<h1 id="heading1">First Heading</h1>
|
||||
<p>Some content</p>
|
||||
<h2 id="heading2">Second Heading</h2>
|
||||
<h3 id="heading3">Third Heading</h3>
|
||||
<p>More content</p>
|
||||
<h2 id="heading4">Fourth Heading</h2>
|
||||
`;
|
||||
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 = `
|
||||
<h1>Chapter 1</h1>
|
||||
<h2>Section 1.1</h2>
|
||||
<h3>Subsection 1.1.1</h3>
|
||||
<h3>Subsection 1.1.2</h3>
|
||||
<h2>Section 1.2</h2>
|
||||
<h1>Chapter 2</h1>
|
||||
`;
|
||||
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 = `
|
||||
<h1 id="target-heading">Target Heading</h1>
|
||||
<p style="height: 1000px;">Spacer content</p>
|
||||
`;
|
||||
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 = `
|
||||
<div style="height: 100px;"></div>
|
||||
<h1 id="section1">Section 1</h1>
|
||||
<div style="height: 400px;"></div>
|
||||
<h2 id="section2">Section 2</h2>
|
||||
<div style="height: 400px;"></div>
|
||||
<h2 id="section3">Section 3</h2>
|
||||
<div style="height: 400px;"></div>
|
||||
`;
|
||||
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 };
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
|
||||
const extractedContainer = document.createElement('div');
|
||||
extractedContainer.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -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('')).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');
|
||||
});
|
||||
}
|
||||
@@ -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 = '<div id="markdown-content"></div>';
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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 = '<div id="markdown-content"></div>';
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocumentNavigator Live Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
scroll-margin-top: 100px; /* Account for navigator */
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #34495e;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #7f8c8d;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: #fff3cd;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #ffc107;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="demo-header">
|
||||
<h1>📋 DocumentNavigator Live Demo</h1>
|
||||
<p>This page demonstrates the Substack-style floating navigation widget in action.</p>
|
||||
<p><strong>Look for the hamburger menu (☰) on the left side!</strong></p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>Features to test:</strong><br>
|
||||
• Click the hamburger menu to expand navigation<br>
|
||||
• Click any heading in the navigator to jump to it<br>
|
||||
• Scroll and watch the current section highlight<br>
|
||||
• Try keyboard shortcuts (Enter/Space to toggle, Escape to close)<br>
|
||||
• Resize window to test responsive behavior
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="markdown-content" class="demo-content">
|
||||
<h1 id="introduction">1. Introduction to MarkiTect</h1>
|
||||
<div class="content-section">
|
||||
<p>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.</p>
|
||||
|
||||
<p>The navigator automatically extracts headings from your content and builds a hierarchical table of contents that floats elegantly on the side of your document.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="features">1.1 Core Features</h2>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator widget includes numerous advanced features designed for optimal user experience:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Automatic Heading Detection</strong>: Scans document for H1, H2, H3 elements</li>
|
||||
<li><strong>Hierarchical Structure</strong>: Maintains proper heading hierarchy with indentation</li>
|
||||
<li><strong>Scroll Spy</strong>: Highlights current section as you scroll</li>
|
||||
<li><strong>Smooth Navigation</strong>: Animated scrolling to clicked sections</li>
|
||||
<li><strong>Responsive Design</strong>: Auto-hides on mobile devices</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 id="responsive">1.1.1 Responsive Behavior</h3>
|
||||
<div class="content-section">
|
||||
<p>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.</p>
|
||||
|
||||
<p>Try resizing your browser window to see this behavior in action. The navigator will disappear when the viewport becomes narrow (under 768px wide).</p>
|
||||
</div>
|
||||
|
||||
<h3 id="accessibility">1.1.2 Accessibility Features</h3>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator is built with accessibility in mind:</p>
|
||||
|
||||
<ul>
|
||||
<li>Full keyboard navigation support</li>
|
||||
<li>ARIA labels and proper semantic markup</li>
|
||||
<li>Screen reader compatibility</li>
|
||||
<li>High contrast hover states</li>
|
||||
<li>Focus management</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="implementation">1.2 Implementation Details</h2>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator is implemented as a modular ES6 class that extends our base UIWidget class. This follows the planned plugin architecture for MarkiTect widgets.</p>
|
||||
|
||||
<p>Key implementation highlights include:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>extractHeadings()</code> - Scans DOM for heading elements</li>
|
||||
<li><code>buildNavigationTree()</code> - Creates hierarchical structure</li>
|
||||
<li><code>handleScroll()</code> - Manages scroll spy functionality</li>
|
||||
<li><code>navigateToHeading()</code> - Handles smooth scrolling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h1 id="architecture">2. Widget Architecture</h1>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator follows a clean architectural pattern that separates concerns and provides maximum flexibility for customization and extension.</p>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="base-classes">2.1 Base Class Hierarchy</h2>
|
||||
<div class="content-section">
|
||||
<p>Our widget system is built on a foundation of base classes that provide common functionality:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Widget</strong>: Core functionality (events, state, lifecycle)</li>
|
||||
<li><strong>UIWidget</strong>: DOM manipulation and visual behavior</li>
|
||||
<li><strong>InteractiveWidget</strong>: Event handling and user interaction</li>
|
||||
</ul>
|
||||
|
||||
<p>DocumentNavigator extends UIWidget directly since it doesn't require complex interaction handling beyond basic click and keyboard events.</p>
|
||||
</div>
|
||||
|
||||
<h3 id="events">2.1.1 Event System</h3>
|
||||
<div class="content-section">
|
||||
<p>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.</p>
|
||||
|
||||
<p>Key events emitted by DocumentNavigator:</p>
|
||||
|
||||
<ul>
|
||||
<li><code>rendered</code> - Widget has been rendered to DOM</li>
|
||||
<li><code>navigate</code> - User navigated to a heading</li>
|
||||
<li><code>toggle</code> - Widget was expanded or collapsed</li>
|
||||
<li><code>theme-changed</code> - Theme was changed</li>
|
||||
<li><code>destroyed</code> - Widget was destroyed</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 id="state">2.1.2 State Management</h3>
|
||||
<div class="content-section">
|
||||
<p>State management is handled through a simple Map-based system that provides reactive updates and event emission when state changes occur.</p>
|
||||
|
||||
<p>This approach is lightweight but powerful enough for most widget use cases while remaining debuggable and predictable.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="plugin-system">2.2 Plugin System Integration</h2>
|
||||
<div class="content-section">
|
||||
<p>While the current implementation works standalone, it's designed to integrate seamlessly with our planned plugin system. The plugin definition includes:</p>
|
||||
|
||||
<ul>
|
||||
<li>Metadata and versioning information</li>
|
||||
<li>Dependency declarations</li>
|
||||
<li>Default configuration options</li>
|
||||
<li>Lifecycle hooks</li>
|
||||
<li>Theme variants</li>
|
||||
<li>Development helpers</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h1 id="usage">3. Usage Examples</h1>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator can be used in several ways, from simple instantiation to advanced configuration with custom themes and behavior.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="basic-usage">3.1 Basic Usage</h2>
|
||||
<div class="content-section">
|
||||
<p>The simplest way to use DocumentNavigator is with default settings:</p>
|
||||
|
||||
<pre><code>const navigator = new DocumentNavigator();
|
||||
await navigator.initialize();
|
||||
await navigator.render();</code></pre>
|
||||
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="advanced-usage">3.2 Advanced Configuration</h2>
|
||||
<div class="content-section">
|
||||
<p>For more control, you can specify detailed configuration options:</p>
|
||||
|
||||
<pre><code>const navigator = new DocumentNavigator({
|
||||
position: 'right',
|
||||
collapsed: false,
|
||||
theme: 'dark',
|
||||
maxHeadingLevel: 4,
|
||||
enableScrollSpy: true,
|
||||
smoothScroll: true
|
||||
});</code></pre>
|
||||
|
||||
<p>This creates a navigator on the right side that starts expanded, includes H4 headings, and uses the dark theme.</p>
|
||||
</div>
|
||||
|
||||
<h3 id="theming">3.2.1 Custom Theming</h3>
|
||||
<div class="content-section">
|
||||
<p>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.</p>
|
||||
|
||||
<p>Available themes include <code>default</code>, <code>dark</code>, and <code>minimal</code>, each optimized for different use cases and aesthetics.</p>
|
||||
</div>
|
||||
|
||||
<h1 id="testing">4. Testing and Quality</h1>
|
||||
<div class="content-section">
|
||||
<p>The DocumentNavigator implementation follows Test-Driven Development (TDD) methodology with comprehensive test coverage ensuring reliability and maintainability.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="test-coverage">4.1 Test Coverage</h2>
|
||||
<div class="content-section">
|
||||
<p>Our test suite covers all major functionality:</p>
|
||||
|
||||
<ul>
|
||||
<li>Widget instantiation and configuration</li>
|
||||
<li>DOM rendering and element creation</li>
|
||||
<li>Heading extraction and hierarchy building</li>
|
||||
<li>Navigation and smooth scrolling</li>
|
||||
<li>Expand/collapse animations</li>
|
||||
<li>Scroll spy functionality</li>
|
||||
<li>Responsive behavior</li>
|
||||
<li>Keyboard navigation</li>
|
||||
<li>Event emission</li>
|
||||
<li>Edge cases and error handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="performance">4.2 Performance Considerations</h2>
|
||||
<div class="content-section">
|
||||
<p>The navigator is optimized for performance with several key strategies:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Throttled Scroll Events</strong>: Scroll spy updates are throttled to 100ms intervals</li>
|
||||
<li><strong>Efficient DOM Queries</strong>: Heading extraction is done once and cached</li>
|
||||
<li><strong>Conditional Rendering</strong>: Navigator only renders if minimum heading count is met</li>
|
||||
<li><strong>Memory Management</strong>: Proper cleanup prevents memory leaks</li>
|
||||
<li><strong>Responsive Loading</strong>: Navigator automatically hides on mobile to save resources</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h1 id="conclusion">5. Conclusion</h1>
|
||||
<div class="content-section">
|
||||
<p>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.</p>
|
||||
|
||||
<p>The implementation demonstrates the power of our widget architecture approach, with clean separation of concerns, comprehensive testing, and excellent extensibility for future enhancements.</p>
|
||||
|
||||
<p><strong>Scroll back to the top and try the navigation features!</strong> The hamburger menu should be visible on the left side of your screen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load widget classes -->
|
||||
<script type="module">
|
||||
// Import our widget classes
|
||||
import { Widget } from '../widgets/base/Widget.js';
|
||||
import { UIWidget } from '../widgets/base/UIWidget.js';
|
||||
import { DocumentNavigator } from '../widgets/navigation/DocumentNavigator.js';
|
||||
|
||||
// Make classes available globally for demo
|
||||
window.Widget = Widget;
|
||||
window.UIWidget = UIWidget;
|
||||
window.DocumentNavigator = DocumentNavigator;
|
||||
|
||||
// Initialize navigator on page load
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('🧭 Initializing DocumentNavigator demo...');
|
||||
|
||||
try {
|
||||
// Create navigator with demo settings
|
||||
const navigator = new DocumentNavigator({
|
||||
container: document.getElementById('markdown-content'),
|
||||
position: 'left',
|
||||
collapsed: true,
|
||||
theme: 'default',
|
||||
enableScrollSpy: true,
|
||||
autoHide: true,
|
||||
maxHeadingLevel: 3,
|
||||
minHeadings: 1 // Show navigator even with few headings for demo
|
||||
});
|
||||
|
||||
// Initialize and render
|
||||
await navigator.initialize();
|
||||
const element = await navigator.render();
|
||||
|
||||
if (element) {
|
||||
console.log('✅ DocumentNavigator initialized successfully!');
|
||||
console.log(` Found ${navigator.headings.length} headings`);
|
||||
console.log(' Click the hamburger menu (☰) to expand navigation');
|
||||
} else {
|
||||
console.log('ℹ️ DocumentNavigator not rendered (insufficient headings)');
|
||||
}
|
||||
|
||||
// Add some debugging helpers
|
||||
window.navigator = navigator;
|
||||
window.testNavigator = {
|
||||
expand: () => navigator.expand(),
|
||||
collapse: () => navigator.collapse(),
|
||||
toggle: () => navigator.toggle(),
|
||||
showHeadings: () => console.table(navigator.headings),
|
||||
showTree: () => console.log(navigator.navigationTree)
|
||||
};
|
||||
|
||||
console.log('🔧 Debugging helpers available:');
|
||||
console.log(' window.navigator - navigator instance');
|
||||
console.log(' window.testNavigator - helper functions');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DocumentNavigator initialization failed:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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 = '<div id="markdown-content"></div>';
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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.
|
||||
@@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="generator" content="Markitect Markitect v0.8.1.dev24+gdbde13e03.d20251111">
|
||||
<title>Test Document</title>
|
||||
|
||||
<!-- Base styling for document content -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Content styling */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #2c3e50;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5em; border-bottom: 3px solid #3498db; padding-bottom: 0.5rem; }
|
||||
h2 { font-size: 2em; border-bottom: 2px solid #3498db; padding-bottom: 0.3rem; }
|
||||
h3 { font-size: 1.5em; color: #34495e; }
|
||||
|
||||
p {
|
||||
margin-bottom: 1.2rem;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #3498db;
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.control-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Control system styles -->
|
||||
<link rel="stylesheet" href="markitect/static/css/controls.css">
|
||||
|
||||
<!-- External dependencies -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
||||
onerror="console.error('CDN library failed to load - network or firewall blocking marked.js'); window.markitectMarkedError = true;"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="markitect-content">
|
||||
<h1 id="test-document">Test Document</h1>
|
||||
<p>This is a test document to check if UI controls appear in edit mode.</p>
|
||||
<h2 id="section-1">Section 1</h2>
|
||||
<p>Some content here.</p>
|
||||
<hr />
|
||||
<p><em>-- html from markdown by <a href="https://coulomb.social/open/MarkiTect" target="_blank">MarkiTect</a> on 2025-11-11 23:42:23 by <a href="https://coulomb.social/open/worsch" target="_blank">worsch</a></em></p>
|
||||
</div>
|
||||
|
||||
<!-- Core JavaScript modules -->
|
||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||
|
||||
<!-- Control system -->
|
||||
<script src="../js/controls/control-base.js"></script>
|
||||
<script src="../js/controls/status-control.js"></script>
|
||||
|
||||
<!-- Main application -->
|
||||
<script src="markitect/static/js/main.js"></script>
|
||||
|
||||
<!-- Handle CDN loading errors -->
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
if (window.markitectMarkedError) {
|
||||
console.error("CDN library failed to load - network or firewall blocking marked.js");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user