+ This test suite follows Test-Driven Development methodology to implement a Substack-style
+ floating document navigation widget. The tests define the expected behavior before
+ implementation begins.
+
+
+
+ Test Coverage:
+
+
β Widget class structure and inheritance
+
β Configuration and initialization
+
β DOM rendering and UI elements
+
β Heading extraction and hierarchy building
+
β Navigation functionality and smooth scrolling
+
β Expand/collapse behavior
+
β Scroll spy and active section detection
+
β Responsive behavior and auto-hide
+
β Keyboard navigation support
+
β Event emission and user interaction
+
β Edge cases and error handling
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Test Chapter 1
+
Sample content for testing heading extraction.
+
Section 1.1
+
Subsection 1.1.1
+
More sample content.
+
Section 1.2
+
Test Chapter 2
+
+
+
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-document-navigator.js b/testdrive-jsui/static/js/tests/test-document-navigator.js
new file mode 100644
index 00000000..e6a79f99
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-document-navigator.js
@@ -0,0 +1,432 @@
+/**
+ * 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 = `
+
+
+ `;
+ document.body.appendChild(testContainer);
+
+ const navigator = new DocumentNavigator({
+ container: testContainer,
+ enableScrollSpy: true
+ });
+ await navigator.render();
+
+ // Test current section detection
+ const currentSection = navigator.getCurrentSection();
+ this.expect(currentSection).toBeTruthy();
+
+ // Cleanup
+ document.body.removeChild(testContainer);
+});
+
+runner.test('DocumentNavigator should handle responsive behavior', async function() {
+ const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
+
+ const navigator = new DocumentNavigator({ autoHide: true });
+ await navigator.render();
+
+ // Mock viewport resize
+ const originalInnerWidth = window.innerWidth;
+
+ // Test mobile viewport
+ Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true });
+ navigator.handleResize();
+ this.expect(navigator.element.style.display).toBe('none');
+
+ // Test desktop viewport
+ Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true });
+ navigator.handleResize();
+ this.expect(navigator.element.style.display).not.toBe('none');
+
+ // Restore original
+ Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true });
+});
+
+runner.test('DocumentNavigator should provide keyboard navigation support', async function() {
+ const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
+
+ const navigator = new DocumentNavigator();
+ await navigator.render();
+
+ // Test keyboard shortcuts
+ let expandCalled = false;
+ let collapseCalled = false;
+
+ navigator.expand = async () => { expandCalled = true; };
+ navigator.collapse = async () => { collapseCalled = true; };
+
+ // Simulate keyboard events
+ const element = navigator.element;
+
+ // Test Escape key (should collapse)
+ const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
+ element.dispatchEvent(escapeEvent);
+ this.expect(collapseCalled).toBeTruthy();
+
+ // Test Enter/Space key (should expand)
+ const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
+ element.dispatchEvent(enterEvent);
+ this.expect(expandCalled).toBeTruthy();
+});
+
+runner.test('DocumentNavigator should emit events for user interactions', async function() {
+ const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
+
+ const navigator = new DocumentNavigator();
+ await navigator.render();
+
+ // Test event emission
+ let navigationEvent = null;
+ navigator.addEventListener('navigate', (e) => {
+ navigationEvent = e;
+ });
+
+ let toggleEvent = null;
+ navigator.addEventListener('toggle', (e) => {
+ toggleEvent = e;
+ });
+
+ // Trigger navigation
+ navigator.navigateToHeading('test-heading');
+ this.expect(navigationEvent).toBeTruthy();
+ this.expect(navigationEvent.detail.target).toBe('test-heading');
+
+ // Trigger toggle
+ await navigator.toggle();
+ this.expect(toggleEvent).toBeTruthy();
+});
+
+runner.test('DocumentNavigator should handle empty document gracefully', async function() {
+ const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
+
+ // Create empty container
+ const emptyContainer = document.createElement('div');
+ document.body.appendChild(emptyContainer);
+
+ const navigator = new DocumentNavigator({ container: emptyContainer });
+
+ const headings = navigator.extractHeadings();
+ this.expect(headings).toHaveLength(0);
+
+ await navigator.render();
+ const navList = navigator.findElement('.navigator-list');
+ this.expect(navList.children).toHaveLength(0);
+
+ // Should show empty state message
+ const emptyMessage = navigator.findElement('.navigator-empty');
+ this.expect(emptyMessage).toBeTruthy();
+
+ // Cleanup
+ document.body.removeChild(emptyContainer);
+});
+
+// Export test runner for use in HTML
+window.runDocumentNavigatorTests = () => runner.run();
+
+console.log('π DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()');
+
+export { runner };
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-documentcontrols-extraction.js b/testdrive-jsui/static/js/tests/test-documentcontrols-extraction.js
new file mode 100644
index 00000000..2d5607ca
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-documentcontrols-extraction.js
@@ -0,0 +1,218 @@
+#!/usr/bin/env node
+
+/**
+ * TDD Test for Document Controls Component Extraction
+ *
+ * Tests the extraction of DocumentControls from the monolithic editor.js
+ * DocumentControls handles the floating control panel and its actions.
+ */
+
+const RefactorTestRunner = require('./refactor-test-runner.js');
+
+const runner = new RefactorTestRunner();
+
+// Define expected DocumentControls API
+const EXPECTED_DOCUMENTCONTROLS_API = [
+ 'constructor',
+ 'create',
+ 'destroy',
+ 'show',
+ 'hide',
+ 'addButton',
+ 'removeButton',
+ 'setEventHandlers',
+ 'updateStatus',
+ 'getControlPanel'
+];
+
+runner.describe('DocumentControls Component Extraction', () => {
+
+ runner.it('should define expected API methods', () => {
+ const expectedMethods = EXPECTED_DOCUMENTCONTROLS_API;
+ runner.expect(expectedMethods.length).toBe(10);
+ runner.expect(expectedMethods).toContain('create');
+ runner.expect(expectedMethods).toContain('addButton');
+ runner.expect(expectedMethods).toContain('setEventHandlers');
+ });
+
+ runner.it('should load extracted DocumentControls component', () => {
+ // Load the extracted component
+ delete require.cache[require.resolve('../components/document-controls.js')];
+
+ try {
+ const module = require('../components/document-controls.js');
+ runner.expect(module.DocumentControls).toBeTruthy();
+
+ // Set global for other tests
+ global.ExtractedDocumentControls = module.DocumentControls;
+ } catch (error) {
+ throw new Error(`Failed to load extracted DocumentControls: ${error.message}`);
+ }
+ });
+
+ runner.it('should preserve constructor functionality', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ runner.expect(controls).toBeInstanceOf(DocumentControls);
+ runner.expect(controls.controlPanel).toBeFalsy(); // Initially null
+ runner.expect(controls.buttons).toBeInstanceOf(Map);
+ });
+
+ runner.it('should preserve control panel creation functionality', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ controls.create();
+
+ const panel = controls.getControlPanel();
+ runner.expect(panel).toBeTruthy();
+ runner.expect(panel.id).toBe('markitect-global-controls');
+
+ // Check that panel is added to DOM
+ const domPanel = document.getElementById('markitect-global-controls');
+ runner.expect(domPanel).toBeTruthy();
+
+ // Cleanup
+ controls.destroy();
+ });
+
+ runner.it('should preserve button creation functionality', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ controls.create();
+
+ // Default buttons should be created
+ runner.expect(controls.buttons.has('save-document')).toBeTruthy();
+ runner.expect(controls.buttons.has('reset-all')).toBeTruthy();
+ runner.expect(controls.buttons.has('show-status')).toBeTruthy();
+ runner.expect(controls.buttons.has('toggle-debug')).toBeTruthy();
+
+ // Check DOM elements exist
+ runner.expect(document.getElementById('save-document')).toBeTruthy();
+ runner.expect(document.getElementById('reset-all')).toBeTruthy();
+ runner.expect(document.getElementById('show-status')).toBeTruthy();
+ runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
+
+ // Cleanup
+ controls.destroy();
+ });
+
+ runner.it('should support custom button addition', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ controls.create();
+
+ // Add custom button
+ const customButton = controls.addButton('custom-test', 'π― Test', '#ff6600');
+ runner.expect(customButton).toBeTruthy();
+ runner.expect(customButton.id).toBe('custom-test');
+ runner.expect(customButton.textContent).toBe('π― Test');
+
+ // Check button is in map and DOM
+ runner.expect(controls.buttons.has('custom-test')).toBeTruthy();
+ runner.expect(document.getElementById('custom-test')).toBeTruthy();
+
+ // Cleanup
+ controls.destroy();
+ });
+
+ runner.it('should support event handler configuration', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ controls.create();
+
+ let saveClicked = false;
+ let resetClicked = false;
+
+ const handlers = {
+ 'save-document': () => { saveClicked = true; },
+ 'reset-all': () => { resetClicked = true; }
+ };
+
+ controls.setEventHandlers(handlers);
+
+ // Simulate button clicks
+ const saveBtn = document.getElementById('save-document');
+ const resetBtn = document.getElementById('reset-all');
+
+ saveBtn.click();
+ resetBtn.click();
+
+ runner.expect(saveClicked).toBeTruthy();
+ runner.expect(resetClicked).toBeTruthy();
+
+ // Cleanup
+ controls.destroy();
+ });
+
+ runner.it('should support show/hide functionality', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ controls.create();
+
+ const panel = controls.getControlPanel();
+
+ // Test hiding
+ controls.hide();
+ runner.expect(panel.style.display).toBe('none');
+
+ // Test showing
+ controls.show();
+ runner.expect(panel.style.display).toBe('block');
+
+ // Cleanup
+ controls.destroy();
+ });
+
+ runner.it('should preserve destroy functionality', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ controls.create();
+
+ // Verify panel exists
+ runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
+
+ // Destroy
+ controls.destroy();
+
+ // Verify panel is removed
+ runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
+ runner.expect(controls.controlPanel).toBeFalsy();
+ });
+
+ runner.it('should support status updates', () => {
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ const controls = new DocumentControls();
+ controls.create();
+
+ // Test status update
+ controls.updateStatus({ totalSections: 5, editingSections: 2 });
+
+ // The status should be reflected in the panel (implementation specific)
+ const panel = controls.getControlPanel();
+ runner.expect(panel).toBeTruthy();
+
+ // Cleanup
+ controls.destroy();
+ });
+});
+
+module.exports = {
+ runner,
+ EXPECTED_DOCUMENTCONTROLS_API
+};
+
+// Run tests if called directly
+if (require.main === module) {
+ console.log('π§ͺ Testing DocumentControls Component Extraction');
+ runner.run().then(() => {
+ console.log('β DocumentControls extraction tests completed');
+ });
+}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-domrenderer-extraction.js b/testdrive-jsui/static/js/tests/test-domrenderer-extraction.js
new file mode 100644
index 00000000..e8aadc04
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-domrenderer-extraction.js
@@ -0,0 +1,212 @@
+#!/usr/bin/env node
+
+/**
+ * TDD Test for DOMRenderer Component Extraction
+ *
+ * Tests the extraction of DOMRenderer from the monolithic editor.js
+ * DOMRenderer handles all DOM interactions and UI rendering.
+ */
+
+const RefactorTestRunner = require('./refactor-test-runner.js');
+
+const runner = new RefactorTestRunner();
+
+// Define expected DOMRenderer API
+const EXPECTED_DOMRENDERER_API = [
+ 'constructor',
+ 'renderAllSections',
+ 'renderSection',
+ 'showEditor',
+ 'hideCurrentEditor',
+ 'showImageEditor',
+ 'findSectionElement',
+ 'handleSectionClick',
+ 'setupSectionElement',
+ 'trackEvent',
+ 'getEventStats'
+ // Note: addGlobalControls and debug methods are on MarkitectCleanEditor, not DOMRenderer
+];
+
+runner.describe('DOMRenderer Component Extraction', () => {
+
+ runner.it('should define expected API methods', () => {
+ const expectedMethods = EXPECTED_DOMRENDERER_API;
+ runner.expect(expectedMethods.length).toBe(11);
+ runner.expect(expectedMethods).toContain('renderAllSections');
+ runner.expect(expectedMethods).toContain('showEditor');
+ runner.expect(expectedMethods).toContain('handleSectionClick');
+ });
+
+ runner.it('should extract from monolithic editor.js', () => {
+ // Load the monolithic editor.js to extract DOMRenderer
+ delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
+
+ try {
+ const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
+ runner.expect(editorModule.DOMRenderer).toBeTruthy();
+ // Set global for other tests
+ global.DOMRenderer = editorModule.DOMRenderer;
+ global.SectionManager = editorModule.SectionManager;
+ } catch (error) {
+ throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
+ }
+ });
+
+ runner.it('should preserve DOMRenderer constructor functionality', () => {
+ const DOMRenderer = global.DOMRenderer;
+ const SectionManager = global.SectionManager;
+
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+
+ const renderer = new DOMRenderer(sectionManager, container);
+ runner.expect(renderer).toBeInstanceOf(DOMRenderer);
+ runner.expect(renderer.sectionManager).toBe(sectionManager);
+ runner.expect(renderer.container).toBe(container);
+ });
+
+ runner.it('should preserve section rendering functionality', () => {
+ const DOMRenderer = global.DOMRenderer;
+ const SectionManager = global.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+
+ // This should not throw an error
+ renderer.renderAllSections(sections);
+
+ // Check that some content was rendered
+ runner.expect(container.innerHTML.length).toBe(container.innerHTML.length); // Basic sanity check
+ });
+
+ runner.it('should preserve findSectionElement functionality', () => {
+ const DOMRenderer = global.DOMRenderer;
+ const SectionManager = global.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ renderer.renderAllSections(sections);
+
+ const sectionId = sections[0].id;
+ const element = renderer.findSectionElement(sectionId);
+
+ // Should find an element or return null (not throw error)
+ runner.expect(typeof element === 'object').toBeTruthy();
+ });
+
+ runner.it('should preserve event tracking functionality', () => {
+ const DOMRenderer = global.DOMRenderer;
+ const SectionManager = global.SectionManager;
+
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ // Should have trackEvent method
+ runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
+
+ // Should be able to track an event
+ renderer.trackEvent('test-event', { data: 'test' });
+
+ // Should have getEventStats method
+ runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
+
+ const stats = renderer.getEventStats();
+ runner.expect(typeof stats === 'object').toBeTruthy();
+ });
+
+ runner.it('should preserve editor showing functionality', () => {
+ const DOMRenderer = global.DOMRenderer;
+ const SectionManager = global.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ renderer.renderAllSections(sections);
+
+ const sectionId = sections[0].id;
+
+ // showEditor should not throw error
+ try {
+ renderer.showEditor(sectionId, 'test content');
+ runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
+ } catch (error) {
+ // Some errors are expected if DOM structure isn't complete
+ runner.expect(typeof error.message === 'string').toBeTruthy();
+ }
+ });
+
+ runner.it('should have core DOM rendering methods', () => {
+ const DOMRenderer = global.DOMRenderer;
+ const SectionManager = global.SectionManager;
+
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ // Should have core methods
+ runner.expect(typeof renderer.renderAllSections === 'function').toBeTruthy();
+ runner.expect(typeof renderer.showEditor === 'function').toBeTruthy();
+ runner.expect(typeof renderer.findSectionElement === 'function').toBeTruthy();
+ runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
+ });
+});
+
+// Export API tests for use during extraction
+const DOMRENDERER_API_TESTS = [
+ (DOMRenderer, SectionManager) => {
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+ if (!renderer.sectionManager) {
+ throw new Error('sectionManager property missing');
+ }
+ },
+ (DOMRenderer, SectionManager) => {
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+ if (typeof renderer.renderAllSections !== 'function') {
+ throw new Error('renderAllSections method missing');
+ }
+ },
+ (DOMRenderer, SectionManager) => {
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+ if (typeof renderer.showEditor !== 'function') {
+ throw new Error('showEditor method missing');
+ }
+ }
+];
+
+module.exports = {
+ runner,
+ EXPECTED_DOMRENDERER_API,
+ DOMRENDERER_API_TESTS
+};
+
+// Run tests if called directly
+if (require.main === module) {
+ console.log('π§ͺ Testing DOMRenderer Component Extraction');
+ runner.run().then(() => {
+ console.log('β DOMRenderer extraction tests completed');
+ });
+}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-extracted-domrenderer.js b/testdrive-jsui/static/js/tests/test-extracted-domrenderer.js
new file mode 100644
index 00000000..d0a8990a
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-extracted-domrenderer.js
@@ -0,0 +1,271 @@
+#!/usr/bin/env node
+
+/**
+ * TDD Test for Extracted DOMRenderer Component
+ *
+ * Tests the extracted DOMRenderer component independently from the monolith.
+ * Verifies that core functionality is preserved after extraction.
+ */
+
+const RefactorTestRunner = require('./refactor-test-runner.js');
+
+const runner = new RefactorTestRunner();
+
+runner.describe('Extracted DOMRenderer Component', () => {
+
+ runner.it('should load extracted DOMRenderer component', () => {
+ // Load the extracted component
+ delete require.cache[require.resolve('../components/dom-renderer.js')];
+
+ try {
+ const module = require('../components/dom-renderer.js');
+ runner.expect(module.DOMRenderer).toBeTruthy();
+ runner.expect(module.FloatingMenu).toBeTruthy();
+
+ // Set globals for other tests
+ global.ExtractedDOMRenderer = module.DOMRenderer;
+ global.ExtractedFloatingMenu = module.FloatingMenu;
+ } catch (error) {
+ throw new Error(`Failed to load extracted DOMRenderer: ${error.message}`);
+ }
+ });
+
+ runner.it('should preserve constructor functionality', () => {
+ const DOMRenderer = global.ExtractedDOMRenderer;
+
+ // Load SectionManager from our extracted core
+ const sectionModule = require('../core/section-manager.js');
+ const SectionManager = sectionModule.SectionManager;
+
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+
+ const renderer = new DOMRenderer(sectionManager, container);
+ runner.expect(renderer).toBeInstanceOf(DOMRenderer);
+ runner.expect(renderer.sectionManager).toBe(sectionManager);
+ runner.expect(renderer.container).toBe(container);
+ runner.expect(renderer.editingSections).toBeInstanceOf(Set);
+ });
+
+ runner.it('should preserve section rendering functionality', () => {
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const sectionModule = require('../core/section-manager.js');
+ const SectionManager = sectionModule.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+
+ // This should not throw an error
+ renderer.renderAllSections(sections);
+
+ // Check that content was rendered
+ runner.expect(container.innerHTML.length > 100).toBeTruthy();
+ runner.expect(container.innerHTML).toContain('Test Heading');
+ });
+
+ runner.it('should preserve findSectionElement functionality', () => {
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const sectionModule = require('../core/section-manager.js');
+ const SectionManager = sectionModule.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ renderer.renderAllSections(sections);
+
+ const sectionId = sections[0].id;
+ const element = renderer.findSectionElement(sectionId);
+
+ runner.expect(element).toBeTruthy();
+ runner.expect(element.getAttribute('data-section-id')).toBe(sectionId);
+ });
+
+ runner.it('should preserve event tracking functionality', () => {
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const sectionModule = require('../core/section-manager.js');
+ const SectionManager = sectionModule.SectionManager;
+
+ const container = document.createElement('div');
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ // Should have trackEvent method
+ runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
+
+ // Should be able to track an event
+ renderer.trackEvent('test-event', { data: 'test' });
+
+ // Should have getEventStats method
+ runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
+
+ const stats = renderer.getEventStats();
+ runner.expect(typeof stats === 'object').toBeTruthy();
+ runner.expect(stats).toHaveProperty('stats');
+ runner.expect(stats).toHaveProperty('totalEvents');
+ runner.expect(stats).toHaveProperty('recentEvents');
+ });
+
+ runner.it('should preserve editor showing functionality', () => {
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const sectionModule = require('../core/section-manager.js');
+ const SectionManager = sectionModule.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ renderer.renderAllSections(sections);
+
+ const sectionId = sections[0].id;
+
+ // showEditor should not throw error
+ try {
+ renderer.showEditor(sectionId, 'test content');
+ runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
+
+ // Check that editing state was set
+ runner.expect(renderer.editingSections.has(sectionId)).toBeTruthy();
+ } catch (error) {
+ throw new Error(`showEditor failed: ${error.message}`);
+ }
+ });
+
+ runner.it('should preserve FloatingMenu functionality', () => {
+ const FloatingMenu = global.ExtractedFloatingMenu;
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const sectionModule = require('../core/section-manager.js');
+ const SectionManager = sectionModule.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ renderer.renderAllSections(sections);
+
+ const sectionId = sections[0].id;
+ const floatingMenu = new FloatingMenu(sectionId, 'text', renderer);
+
+ runner.expect(floatingMenu.sectionId).toBe(sectionId);
+ runner.expect(floatingMenu.type).toBe('text');
+ runner.expect(floatingMenu.renderer).toBe(renderer);
+ runner.expect(floatingMenu.isVisible).toBeFalsy();
+
+ // Test show/hide functionality
+ const content = document.createElement('div');
+ content.textContent = 'Test content';
+
+ floatingMenu.show(content);
+ runner.expect(floatingMenu.isVisible).toBeTruthy();
+
+ floatingMenu.hide();
+ runner.expect(floatingMenu.isVisible).toBeFalsy();
+ });
+
+ runner.it('should handle section click events', () => {
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const sectionModule = require('../core/section-manager.js');
+ const SectionManager = sectionModule.SectionManager;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+
+ const sectionManager = new SectionManager();
+ const renderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = '# Test Heading\nTest content';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ renderer.renderAllSections(sections);
+
+ const sectionId = sections[0].id;
+ const element = renderer.findSectionElement(sectionId);
+
+ // Simulate a click event
+ const clickEvent = new Event('click', { bubbles: true });
+ Object.defineProperty(clickEvent, 'target', { value: element });
+
+ // Should not throw error
+ try {
+ renderer.handleSectionClick(clickEvent);
+ runner.expect(true).toBeTruthy();
+ } catch (error) {
+ throw new Error(`handleSectionClick failed: ${error.message}`);
+ }
+ });
+
+ // Comparative test - verify extracted component behaves similarly to original
+ runner.it('should behave similarly to original monolithic component', () => {
+ // Load both components
+ const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
+ const extractedModule = require('../components/dom-renderer.js');
+ const sectionModule = require('../core/section-manager.js');
+
+ const originalSectionManager = new originalModule.SectionManager();
+ const extractedSectionManager = new sectionModule.SectionManager();
+
+ const originalContainer = document.createElement('div');
+ originalContainer.innerHTML = '';
+
+ const extractedContainer = document.createElement('div');
+ extractedContainer.innerHTML = '';
+
+ const originalRenderer = new originalModule.DOMRenderer(originalSectionManager, originalContainer);
+ const extractedRenderer = new extractedModule.DOMRenderer(extractedSectionManager, extractedContainer);
+
+ const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
+
+ // Create sections with both
+ const originalSections = originalSectionManager.createSectionsFromMarkdown(testMarkdown);
+ const extractedSections = extractedSectionManager.createSectionsFromMarkdown(testMarkdown);
+
+ // Render with both
+ originalRenderer.renderAllSections(originalSections);
+ extractedRenderer.renderAllSections(extractedSections);
+
+ // Should have rendered content
+ runner.expect(originalContainer.innerHTML.length > 100).toBeTruthy();
+ runner.expect(extractedContainer.innerHTML.length > 100).toBeTruthy();
+
+ // Should have same number of section elements
+ const originalSectionElements = originalContainer.querySelectorAll('.ui-edit-section');
+ const extractedSectionElements = extractedContainer.querySelectorAll('.ui-edit-section');
+
+ runner.expect(extractedSectionElements.length).toBe(originalSectionElements.length);
+
+ // Should have similar event stats structure
+ const originalStats = originalRenderer.getEventStats();
+ const extractedStats = extractedRenderer.getEventStats();
+
+ runner.expect(extractedStats).toHaveProperty('stats');
+ runner.expect(extractedStats).toHaveProperty('totalEvents');
+ runner.expect(extractedStats).toHaveProperty('recentEvents');
+ });
+});
+
+module.exports = runner;
+
+// Run tests if called directly
+if (require.main === module) {
+ console.log('π§ͺ Testing Extracted DOMRenderer Component');
+ runner.run().then(() => {
+ console.log('β Extracted DOMRenderer tests completed');
+ });
+}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-extracted-section-manager.js b/testdrive-jsui/static/js/tests/test-extracted-section-manager.js
new file mode 100644
index 00000000..0eb51d01
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-extracted-section-manager.js
@@ -0,0 +1,226 @@
+#!/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');
+ });
+}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-full-integration.js b/testdrive-jsui/static/js/tests/test-full-integration.js
new file mode 100644
index 00000000..3edb0ced
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-full-integration.js
@@ -0,0 +1,305 @@
+#!/usr/bin/env node
+
+/**
+ * Full Integration Test
+ *
+ * Tests that all extracted components (SectionManager, DOMRenderer,
+ * DebugPanel, DocumentControls) work together as a complete system.
+ */
+
+const RefactorTestRunner = require('./refactor-test-runner.js');
+
+const runner = new RefactorTestRunner();
+
+runner.describe('Full Component Integration Tests', () => {
+
+ runner.it('should load all extracted components', () => {
+ try {
+ // Load all extracted components
+ const sectionModule = require('../core/section-manager.js');
+ const domModule = require('../components/dom-renderer.js');
+ const debugModule = require('../components/debug-panel.js');
+ const controlsModule = require('../components/document-controls.js');
+
+ runner.expect(sectionModule.SectionManager).toBeTruthy();
+ runner.expect(domModule.DOMRenderer).toBeTruthy();
+ runner.expect(debugModule.DebugPanel).toBeTruthy();
+ runner.expect(controlsModule.DocumentControls).toBeTruthy();
+
+ // Set globals for other tests
+ global.ExtractedSectionManager = sectionModule.SectionManager;
+ global.ExtractedDOMRenderer = domModule.DOMRenderer;
+ global.ExtractedDebugPanel = debugModule.DebugPanel;
+ global.ExtractedDocumentControls = controlsModule.DocumentControls;
+
+ } catch (error) {
+ throw new Error(`Failed to load extracted components: ${error.message}`);
+ }
+ });
+
+ runner.it('should support complete document editing workflow with all components', () => {
+ const SectionManager = global.ExtractedSectionManager;
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const DebugPanel = global.ExtractedDebugPanel;
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ // Setup DOM container
+ const container = document.createElement('div');
+ container.innerHTML = '';
+ document.body.appendChild(container);
+
+ // Create all components
+ const sectionManager = new SectionManager();
+ const domRenderer = new DOMRenderer(sectionManager, container);
+ const debugPanel = new DebugPanel();
+ const documentControls = new DocumentControls();
+
+ // Setup document controls
+ documentControls.create();
+
+ // Wire up event handlers for debugging
+ sectionManager.on('sections-created', (data) => {
+ debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
+ });
+
+ sectionManager.on('edit-started', (data) => {
+ debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
+ });
+
+ // Test workflow: Create document
+ const testMarkdown = `# Document Title
+Introduction paragraph with some content.
+
+## Section A
+Content for section A with details.
+
+
+
+### Subsection A.1
+More detailed content here.`;
+
+ // Create sections
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ runner.expect(sections.length).toBe(4);
+
+ // Render sections
+ domRenderer.renderAllSections(sections);
+ const renderedElements = container.querySelectorAll('.ui-edit-section');
+ runner.expect(renderedElements.length).toBe(sections.length);
+
+ // Test editing workflow
+ const firstSection = sections[0];
+ sectionManager.startEditing(firstSection.id);
+ runner.expect(firstSection.isEditing()).toBeTruthy();
+
+ // Check debug messages were created
+ runner.expect(debugPanel.getMessageCount()).toBe(2); // sections-created + edit-started
+
+ // Test document controls functionality
+ const controlPanel = documentControls.getControlPanel();
+ runner.expect(controlPanel).toBeTruthy();
+ runner.expect(document.getElementById('save-document')).toBeTruthy();
+ runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
+
+ // Cleanup
+ document.body.removeChild(container);
+ documentControls.destroy();
+ });
+
+ runner.it('should support debug panel integration with document controls', () => {
+ const DebugPanel = global.ExtractedDebugPanel;
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ // Create components
+ const debugPanel = new DebugPanel();
+ const documentControls = new DocumentControls();
+
+ // Setup document controls
+ documentControls.create();
+
+ // Setup debug panel toggle handler
+ const handlers = {
+ 'toggle-debug': () => debugPanel.toggle()
+ };
+ documentControls.setEventHandlers(handlers);
+
+ // Test debug toggle functionality
+ const debugButton = documentControls.getButton('toggle-debug');
+ runner.expect(debugButton).toBeTruthy();
+
+ // Add some debug messages
+ debugPanel.addMessage('Test message 1', 'INFO');
+ debugPanel.addMessage('Test message 2', 'ERROR');
+
+ // Simulate button click to show debug panel
+ debugButton.click();
+ runner.expect(debugPanel.isActive).toBeTruthy();
+
+ // Simulate button click to hide debug panel
+ debugButton.click();
+ runner.expect(debugPanel.isActive).toBeFalsy();
+
+ // Cleanup
+ documentControls.destroy();
+ });
+
+ runner.it('should support event-driven communication between all components', () => {
+ const SectionManager = global.ExtractedSectionManager;
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const DebugPanel = global.ExtractedDebugPanel;
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ // Setup container
+ const container = document.createElement('div');
+ container.innerHTML = '';
+ document.body.appendChild(container);
+
+ // Create components
+ const sectionManager = new SectionManager();
+ const domRenderer = new DOMRenderer(sectionManager, container);
+ const debugPanel = new DebugPanel();
+ const documentControls = new DocumentControls();
+
+ documentControls.create();
+
+ // Setup comprehensive event handling
+ let eventLog = [];
+
+ sectionManager.on('sections-created', (data) => {
+ eventLog.push(`sections-created: ${data.count} sections`);
+ debugPanel.addMessage(`Sections created: ${data.count}`, 'INFO');
+ });
+
+ sectionManager.on('edit-started', (data) => {
+ eventLog.push(`edit-started: ${data.sectionId}`);
+ debugPanel.addMessage(`Edit started: ${data.sectionId}`, 'DEBUG');
+ });
+
+ sectionManager.on('changes-accepted', (data) => {
+ eventLog.push(`changes-accepted: ${data.sectionId}`);
+ debugPanel.addMessage(`Changes accepted: ${data.sectionId}`, 'SUCCESS');
+ });
+
+ // Test complete workflow
+ const testMarkdown = '# Test\nContent for testing';
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ domRenderer.renderAllSections(sections);
+
+ // Start editing
+ sectionManager.startEditing(sections[0].id);
+ sectionManager.updateContent(sections[0].id, '# Updated Test\nUpdated content');
+ sectionManager.acceptChanges(sections[0].id);
+
+ // Verify events were logged
+ runner.expect(eventLog.length).toBe(3);
+ runner.expect(eventLog[0]).toContain('sections-created');
+ runner.expect(eventLog[1]).toContain('edit-started');
+ runner.expect(eventLog[2]).toContain('changes-accepted');
+
+ // Verify debug messages were created
+ runner.expect(debugPanel.getMessageCount()).toBe(3);
+
+ // Test document controls status update
+ const status = sectionManager.getDocumentStatus();
+ documentControls.updateStatus(status);
+ runner.expect(documentControls.lastStatus).toBeTruthy();
+
+ // Cleanup
+ document.body.removeChild(container);
+ documentControls.destroy();
+ });
+
+ runner.it('should handle error scenarios gracefully across components', () => {
+ const SectionManager = global.ExtractedSectionManager;
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const DebugPanel = global.ExtractedDebugPanel;
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ // Test component creation without proper DOM setup
+ const debugPanel = new DebugPanel();
+ const documentControls = new DocumentControls();
+
+ // These should not throw errors
+ try {
+ debugPanel.toggle(); // No DOM elements
+ debugPanel.update(); // No DOM elements
+ documentControls.show(); // No control panel created yet
+ documentControls.hide(); // No control panel created yet
+
+ runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
+ } catch (error) {
+ throw new Error(`Components should handle missing DOM gracefully: ${error.message}`);
+ }
+
+ // Test section manager with invalid input
+ const sectionManager = new SectionManager();
+ const sections = sectionManager.createSectionsFromMarkdown('');
+ runner.expect(sections.length).toBe(0);
+
+ // Test DOM renderer with invalid container
+ try {
+ const invalidRenderer = new DOMRenderer(sectionManager, null);
+ runner.expect(invalidRenderer.container).toBeFalsy();
+ } catch (error) {
+ // This is acceptable - constructor might validate input
+ runner.expect(typeof error.message === 'string').toBeTruthy();
+ }
+ });
+
+ runner.it('should support scalable architecture with component lifecycle', () => {
+ const SectionManager = global.ExtractedSectionManager;
+ const DOMRenderer = global.ExtractedDOMRenderer;
+ const DebugPanel = global.ExtractedDebugPanel;
+ const DocumentControls = global.ExtractedDocumentControls;
+
+ // Test multiple instances
+ const sectionManager1 = new SectionManager();
+ const sectionManager2 = new SectionManager();
+ const debugPanel1 = new DebugPanel();
+ const debugPanel2 = new DebugPanel();
+
+ // Each should be independent
+ debugPanel1.addMessage('Message from panel 1', 'INFO');
+ debugPanel2.addMessage('Message from panel 2', 'ERROR');
+
+ runner.expect(debugPanel1.getMessageCount()).toBe(1);
+ runner.expect(debugPanel2.getMessageCount()).toBe(1);
+
+ // Test section managers are independent
+ const sections1 = sectionManager1.createSectionsFromMarkdown('# Document 1');
+ const sections2 = sectionManager2.createSectionsFromMarkdown('# Document 2');
+
+ runner.expect(sections1.length).toBe(1);
+ runner.expect(sections2.length).toBe(1);
+ runner.expect(sections1[0]).toBeTruthy();
+ runner.expect(sections2[0]).toBeTruthy();
+
+ // IDs should be different (each section gets unique ID)
+ const id1 = sections1[0].id;
+ const id2 = sections2[0].id;
+ runner.expect(id1 !== id2).toBeTruthy();
+
+ // Test document controls lifecycle
+ const controls1 = new DocumentControls();
+ const controls2 = new DocumentControls();
+
+ controls1.create();
+ runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
+
+ controls2.create(); // Should replace the first one
+ runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
+
+ controls2.destroy();
+ runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
+ });
+});
+
+module.exports = runner;
+
+// Run tests if called directly
+if (require.main === module) {
+ console.log('π§ͺ Running Full Component Integration Tests');
+ runner.run().then(() => {
+ console.log('β Full integration tests completed');
+ });
+}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-navigator-demo.html b/testdrive-jsui/static/js/tests/test-navigator-demo.html
new file mode 100644
index 00000000..020178b1
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-navigator-demo.html
@@ -0,0 +1,342 @@
+
+
+
+
+
+ DocumentNavigator Live Demo
+
+
+
+
+
π DocumentNavigator Live Demo
+
This page demonstrates the Substack-style floating navigation widget in action.
+
Look for the hamburger menu (β°) on the left side!
+
+
+ Features to test:
+ β’ Click the hamburger menu to expand navigation
+ β’ Click any heading in the navigator to jump to it
+ β’ Scroll and watch the current section highlight
+ β’ Try keyboard shortcuts (Enter/Space to toggle, Escape to close)
+ β’ Resize window to test responsive behavior
+
+
+
+
+
1. Introduction to MarkiTect
+
+
MarkiTect is an advanced markdown processing engine that provides sophisticated document management capabilities. This demo showcases the DocumentNavigator widget, which provides Substack-style navigation for long-form documents.
+
+
The navigator automatically extracts headings from your content and builds a hierarchical table of contents that floats elegantly on the side of your document.
+
+
+
1.1 Core Features
+
+
The DocumentNavigator widget includes numerous advanced features designed for optimal user experience:
+
+
+
Automatic Heading Detection: Scans document for H1, H2, H3 elements
+
Hierarchical Structure: Maintains proper heading hierarchy with indentation
+
Scroll Spy: Highlights current section as you scroll
+
Smooth Navigation: Animated scrolling to clicked sections
+
Responsive Design: Auto-hides on mobile devices
+
+
+
+
1.1.1 Responsive Behavior
+
+
The navigator intelligently adapts to different screen sizes. On desktop computers, it remains visible as a floating panel. On mobile devices, it automatically hides to preserve screen real estate for content.
+
+
Try resizing your browser window to see this behavior in action. The navigator will disappear when the viewport becomes narrow (under 768px wide).
+
+
+
1.1.2 Accessibility Features
+
+
The DocumentNavigator is built with accessibility in mind:
+
+
+
Full keyboard navigation support
+
ARIA labels and proper semantic markup
+
Screen reader compatibility
+
High contrast hover states
+
Focus management
+
+
+
+
1.2 Implementation Details
+
+
The DocumentNavigator is implemented as a modular ES6 class that extends our base UIWidget class. This follows the planned plugin architecture for MarkiTect widgets.
+
+
Key implementation highlights include:
+
+
+
extractHeadings() - Scans DOM for heading elements
The DocumentNavigator follows a clean architectural pattern that separates concerns and provides maximum flexibility for customization and extension.
+
+
The widget is designed as part of a larger plugin ecosystem that will allow developers to create custom UI components that can be loaded dynamically and configured independently.
+
+
+
2.1 Base Class Hierarchy
+
+
Our widget system is built on a foundation of base classes that provide common functionality:
InteractiveWidget: Event handling and user interaction
+
+
+
DocumentNavigator extends UIWidget directly since it doesn't require complex interaction handling beyond basic click and keyboard events.
+
+
+
2.1.1 Event System
+
+
The widget uses a custom event system built on the native EventTarget API. This allows for clean separation of concerns and easy integration with other components.
+
+
Key events emitted by DocumentNavigator:
+
+
+
rendered - Widget has been rendered to DOM
+
navigate - User navigated to a heading
+
toggle - Widget was expanded or collapsed
+
theme-changed - Theme was changed
+
destroyed - Widget was destroyed
+
+
+
+
2.1.2 State Management
+
+
State management is handled through a simple Map-based system that provides reactive updates and event emission when state changes occur.
+
+
This approach is lightweight but powerful enough for most widget use cases while remaining debuggable and predictable.
+
+
+
2.2 Plugin System Integration
+
+
While the current implementation works standalone, it's designed to integrate seamlessly with our planned plugin system. The plugin definition includes:
+
+
+
Metadata and versioning information
+
Dependency declarations
+
Default configuration options
+
Lifecycle hooks
+
Theme variants
+
Development helpers
+
+
+
+
3. Usage Examples
+
+
The DocumentNavigator can be used in several ways, from simple instantiation to advanced configuration with custom themes and behavior.
+
+
+
3.1 Basic Usage
+
+
The simplest way to use DocumentNavigator is with default settings:
+
+
const navigator = new DocumentNavigator();
+await navigator.initialize();
+await navigator.render();
+
+
This creates a navigator with default settings that will scan the entire document for headings and display them in a collapsible panel on the left side.
+
+
+
3.2 Advanced Configuration
+
+
For more control, you can specify detailed configuration options:
This creates a navigator on the right side that starts expanded, includes H4 headings, and uses the dark theme.
+
+
+
3.2.1 Custom Theming
+
+
The navigator supports multiple built-in themes and can be extended with custom themes. The theming system integrates with MarkiTect's document themes for consistent styling.
+
+
Available themes include default, dark, and minimal, each optimized for different use cases and aesthetics.
+
+
+
4. Testing and Quality
+
+
The DocumentNavigator implementation follows Test-Driven Development (TDD) methodology with comprehensive test coverage ensuring reliability and maintainability.
+
+
+
4.1 Test Coverage
+
+
Our test suite covers all major functionality:
+
+
+
Widget instantiation and configuration
+
DOM rendering and element creation
+
Heading extraction and hierarchy building
+
Navigation and smooth scrolling
+
Expand/collapse animations
+
Scroll spy functionality
+
Responsive behavior
+
Keyboard navigation
+
Event emission
+
Edge cases and error handling
+
+
+
+
4.2 Performance Considerations
+
+
The navigator is optimized for performance with several key strategies:
+
+
+
Throttled Scroll Events: Scroll spy updates are throttled to 100ms intervals
+
Efficient DOM Queries: Heading extraction is done once and cached
+
Conditional Rendering: Navigator only renders if minimum heading count is met
Responsive Loading: Navigator automatically hides on mobile to save resources
+
+
+
+
5. Conclusion
+
+
The DocumentNavigator widget successfully brings Substack-style navigation to MarkiTect documents. It provides an intuitive, accessible, and performant way for users to navigate long-form content.
+
+
The implementation demonstrates the power of our widget architecture approach, with clean separation of concerns, comprehensive testing, and excellent extensibility for future enhancements.
+
+
Scroll back to the top and try the navigation features! The hamburger menu should be visible on the left side of your screen.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-real-user-functionality.js b/testdrive-jsui/static/js/tests/test-real-user-functionality.js
new file mode 100644
index 00000000..3d7fddef
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-real-user-functionality.js
@@ -0,0 +1,285 @@
+#!/usr/bin/env node
+
+/**
+ * Real User Functionality Tests
+ *
+ * This test file validates the actual functionality that users experience,
+ * not just internal API calls. It tests the complete user workflow.
+ */
+
+const RefactorTestRunner = require('./refactor-test-runner.js');
+
+const runner = new RefactorTestRunner();
+
+runner.describe('Real User Functionality Tests', () => {
+
+ runner.it('should allow users to edit content and see changes in DOM', () => {
+ // Load all extracted components
+ const sectionModule = require('../core/section-manager.js');
+ const domModule = require('../components/dom-renderer.js');
+ const debugModule = require('../components/debug-panel.js');
+ const controlsModule = require('../components/document-controls.js');
+
+ const { SectionManager } = sectionModule;
+ const { DOMRenderer } = domModule;
+ const { DebugPanel } = debugModule;
+ const { DocumentControls } = controlsModule;
+
+ // Setup DOM container
+ const container = document.createElement('div');
+ container.innerHTML = '';
+ document.body.appendChild(container);
+
+ // Create components
+ const sectionManager = new SectionManager();
+ const domRenderer = new DOMRenderer(sectionManager, container);
+ const debugPanel = new DebugPanel();
+ const documentControls = new DocumentControls();
+
+ // Setup document controls
+ documentControls.create();
+
+ // Create sections from test markdown
+ const testMarkdown = `# Original Title\nOriginal content that should be editable.`;
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ domRenderer.renderAllSections(sections);
+
+ const firstSection = sections[0];
+ const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
+
+ // Verify original content is rendered
+ runner.expect(sectionElement.innerHTML).toContain('Original Title');
+
+ // Simulate user clicking on section
+ const clickEvent = new Event('click', { bubbles: true });
+ sectionElement.dispatchEvent(clickEvent);
+
+ // Verify editing state is active
+ runner.expect(firstSection.isEditing()).toBeTruthy();
+
+ // Find the floating menu and edit controls
+ const floatingMenu = document.querySelector('.ui-edit-floating-menu');
+ runner.expect(floatingMenu).toBeTruthy();
+
+ const textarea = floatingMenu.querySelector('textarea');
+ const acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
+
+ runner.expect(textarea).toBeTruthy();
+ runner.expect(acceptButton).toBeTruthy();
+
+ // Simulate user editing content
+ const newContent = '# Updated Title\nCompletely new content added by user.';
+ textarea.value = newContent;
+
+ // Simulate user clicking accept
+ acceptButton.click();
+
+ // Verify section is no longer editing
+ runner.expect(firstSection.isEditing()).toBeFalsy();
+
+ // Verify floating menu is gone
+ const menuAfterAccept = document.querySelector('.ui-edit-floating-menu');
+ runner.expect(menuAfterAccept).toBeFalsy();
+
+ // CRITICAL TEST: Verify DOM was actually updated with new content
+ const updatedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
+ runner.expect(updatedElement.innerHTML).toContain('Updated Title');
+ runner.expect(updatedElement.innerHTML).toContain('Completely new content');
+ runner.expect(updatedElement.innerHTML).not.toContain('Original Title');
+
+ // Cleanup
+ document.body.removeChild(container);
+ documentControls.destroy();
+ });
+
+ runner.it('should allow users to reset all changes', () => {
+ // Setup similar to above
+ const sectionModule = require('../core/section-manager.js');
+ const domModule = require('../components/dom-renderer.js');
+ const controlsModule = require('../components/document-controls.js');
+
+ const { SectionManager } = sectionModule;
+ const { DOMRenderer } = domModule;
+ const { DocumentControls } = controlsModule;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+ document.body.appendChild(container);
+
+ const sectionManager = new SectionManager();
+ const domRenderer = new DOMRenderer(sectionManager, container);
+ const documentControls = new DocumentControls();
+
+ documentControls.create();
+
+ // Create and modify content
+ const testMarkdown = `# Test Section\nOriginal content for reset test.`;
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ domRenderer.renderAllSections(sections);
+
+ const firstSection = sections[0];
+
+ // Make changes to the section
+ sectionManager.startEditing(firstSection.id);
+ sectionManager.updateContent(firstSection.id, '# Modified Title\nModified content.');
+ sectionManager.acceptChanges(firstSection.id);
+
+ // Verify changes are applied
+ let sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
+ runner.expect(sectionElement.innerHTML).toContain('Modified Title');
+ runner.expect(firstSection.hasChanges()).toBeTruthy();
+
+ // Test reset functionality
+ const resetButton = documentControls.getButton('reset-all');
+ runner.expect(resetButton).toBeTruthy();
+
+ // Click reset button
+ resetButton.click();
+
+ // Verify content is reset
+ sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
+ runner.expect(sectionElement.innerHTML).toContain('Test Section');
+ runner.expect(sectionElement.innerHTML).not.toContain('Modified Title');
+ runner.expect(firstSection.hasChanges()).toBeFalsy();
+
+ // Cleanup
+ document.body.removeChild(container);
+ documentControls.destroy();
+ });
+
+ runner.it('should handle cancel operations correctly', () => {
+ const sectionModule = require('../core/section-manager.js');
+ const domModule = require('../components/dom-renderer.js');
+
+ const { SectionManager } = sectionModule;
+ const { DOMRenderer } = domModule;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+ document.body.appendChild(container);
+
+ const sectionManager = new SectionManager();
+ const domRenderer = new DOMRenderer(sectionManager, container);
+
+ const testMarkdown = `# Cancel Test\nContent that should remain unchanged.`;
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ domRenderer.renderAllSections(sections);
+
+ const firstSection = sections[0];
+ const originalContent = firstSection.currentMarkdown;
+
+ // Start editing
+ const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
+ sectionElement.click();
+
+ // Make changes but cancel them
+ const floatingMenu = document.querySelector('.ui-edit-floating-menu');
+ const textarea = floatingMenu.querySelector('textarea');
+ const cancelButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Cancel'));
+
+ textarea.value = '# This should be cancelled\nThis content should not appear.';
+ cancelButton.click();
+
+ // Verify content is unchanged
+ const unchangedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
+ runner.expect(unchangedElement.innerHTML).toContain('Cancel Test');
+ runner.expect(unchangedElement.innerHTML).not.toContain('This should be cancelled');
+ runner.expect(firstSection.currentMarkdown).toBe(originalContent);
+
+ // Cleanup
+ document.body.removeChild(container);
+ });
+
+ runner.it('should validate the complete editing workflow', () => {
+ // This test validates the entire user experience end-to-end
+ const sectionModule = require('../core/section-manager.js');
+ const domModule = require('../components/dom-renderer.js');
+ const debugModule = require('../components/debug-panel.js');
+ const controlsModule = require('../components/document-controls.js');
+
+ const { SectionManager } = sectionModule;
+ const { DOMRenderer } = domModule;
+ const { DebugPanel } = debugModule;
+ const { DocumentControls } = controlsModule;
+
+ const container = document.createElement('div');
+ container.innerHTML = '';
+ document.body.appendChild(container);
+
+ const sectionManager = new SectionManager();
+ const domRenderer = new DOMRenderer(sectionManager, container);
+ const debugPanel = new DebugPanel();
+ const documentControls = new DocumentControls();
+
+ documentControls.create();
+
+ // Multi-section document
+ const testMarkdown = `# Document Title
+Introduction paragraph.
+
+## Section A
+Content for section A.
+
+## Section B
+Content for section B.`;
+
+ const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
+ domRenderer.renderAllSections(sections);
+
+ // Verify all sections are rendered
+ const renderedSections = container.querySelectorAll('.ui-edit-section');
+ runner.expect(renderedSections.length).toBe(sections.length);
+
+ // Test editing multiple sections
+ const firstSection = sections[0];
+ const secondSection = sections[2]; // Section A
+
+ // Edit first section
+ renderedSections[0].click();
+ let floatingMenu = document.querySelector('.ui-edit-floating-menu');
+ let textarea = floatingMenu.querySelector('textarea');
+ let acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
+
+ textarea.value = '# Updated Document Title\nUpdated introduction.';
+ acceptButton.click();
+
+ // Edit second section
+ renderedSections[2].click();
+ floatingMenu = document.querySelector('.ui-edit-floating-menu');
+ textarea = floatingMenu.querySelector('textarea');
+ acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
+
+ textarea.value = '## Updated Section A\nCompletely new content for section A.';
+ acceptButton.click();
+
+ // Verify both sections were updated
+ const updatedSections = container.querySelectorAll('.ui-edit-section');
+ runner.expect(updatedSections[0].innerHTML).toContain('Updated Document Title');
+ runner.expect(updatedSections[2].innerHTML).toContain('Updated Section A');
+
+ // Test reset restores all sections
+ const resetButton = documentControls.getButton('reset-all');
+ resetButton.click();
+
+ const resetSections = container.querySelectorAll('.ui-edit-section');
+ runner.expect(resetSections[0].innerHTML).toContain('Document Title');
+ runner.expect(resetSections[0].innerHTML).not.toContain('Updated Document Title');
+ runner.expect(resetSections[2].innerHTML).toContain('Section A');
+ runner.expect(resetSections[2].innerHTML).not.toContain('Updated Section A');
+
+ // Cleanup
+ document.body.removeChild(container);
+ documentControls.destroy();
+ });
+});
+
+module.exports = runner;
+
+// Run tests if called directly
+if (require.main === module) {
+ console.log('π§ͺ Running Real User Functionality Tests');
+ runner.run().then(() => {
+ console.log('β Real user functionality tests completed');
+ console.log('These tests validate what users actually experience, not just internal APIs');
+ });
+}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test-section-manager-extraction.js b/testdrive-jsui/static/js/tests/test-section-manager-extraction.js
new file mode 100644
index 00000000..1eecce5d
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test-section-manager-extraction.js
@@ -0,0 +1,196 @@
+#!/usr/bin/env node
+
+/**
+ * TDD Test for SectionManager Component Extraction
+ *
+ * Tests the extraction of SectionManager from the monolithic editor.js
+ * Ensures all functionality is preserved during refactoring.
+ */
+
+const RefactorTestRunner = require('./refactor-test-runner.js');
+
+const runner = new RefactorTestRunner();
+
+// First, let's define what the SectionManager API should look like
+const EXPECTED_SECTION_MANAGER_API = [
+ 'constructor',
+ 'createSectionsFromMarkdown',
+ 'startEditing',
+ 'stopEditing',
+ 'getAllSections',
+ 'sections', // Map property, not method
+ 'getDocumentStatus',
+ 'getDocumentMarkdown',
+ 'on', // event system
+ 'emit', // event system
+ 'handleSectionSplit',
+ 'updateContent',
+ 'acceptChanges',
+ 'cancelChanges',
+ 'resetSection'
+];
+
+runner.describe('SectionManager Component Extraction', () => {
+
+ runner.it('should define expected API methods', () => {
+ // This test defines what we expect from the extracted SectionManager
+ const expectedMethods = EXPECTED_SECTION_MANAGER_API;
+ runner.expect(expectedMethods.length).toBe(15);
+ runner.expect(expectedMethods).toContain('createSectionsFromMarkdown');
+ runner.expect(expectedMethods).toContain('startEditing');
+ runner.expect(expectedMethods).toContain('stopEditing');
+ });
+
+ runner.it('should extract from monolithic editor.js', () => {
+ // Load the monolithic editor.js to extract SectionManager
+ delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
+
+ try {
+ const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
+ runner.expect(editorModule.SectionManager).toBeTruthy();
+ // Set global for other tests
+ global.SectionManager = editorModule.SectionManager;
+ global.Section = editorModule.Section;
+ global.EditState = editorModule.EditState;
+ } catch (error) {
+ throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
+ }
+ });
+
+ runner.it('should preserve SectionManager constructor functionality', () => {
+ const SectionManager = global.SectionManager;
+
+ const manager = new SectionManager();
+ runner.expect(manager).toBeInstanceOf(SectionManager);
+ runner.expect(manager.sections).toBeInstanceOf(Map);
+ });
+
+ runner.it('should preserve createSectionsFromMarkdown functionality', () => {
+ const SectionManager = global.SectionManager;
+ const manager = new SectionManager();
+
+ const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
+ const sections = manager.createSectionsFromMarkdown(testMarkdown);
+
+ runner.expect(Array.isArray(sections)).toBeTruthy();
+ runner.expect(sections.length).toBe(2);
+ runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
+ runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
+ });
+
+ runner.it('should preserve section editing state management', () => {
+ const SectionManager = global.SectionManager;
+ const manager = new SectionManager();
+
+ const sections = manager.createSectionsFromMarkdown('# Test\nContent');
+ const sectionId = sections[0].id;
+
+ // Test start editing
+ runner.expect(manager.startEditing(sectionId)).toBeTruthy();
+ const section = manager.sections.get(sectionId);
+ runner.expect(section.isEditing()).toBeTruthy();
+
+ // Test stop editing
+ section.stopEditing();
+ runner.expect(section.isEditing()).toBeFalsy();
+ });
+
+ runner.it('should preserve event system functionality', () => {
+ const SectionManager = global.SectionManager;
+ const manager = new SectionManager();
+
+ let eventFired = false;
+ let eventData = null;
+
+ manager.on('test-event', (data) => {
+ eventFired = true;
+ eventData = data;
+ });
+
+ manager.emit('test-event', { test: 'data' });
+
+ runner.expect(eventFired).toBeTruthy();
+ runner.expect(eventData).toEqual({ test: 'data' });
+ });
+
+ runner.it('should preserve document status functionality', () => {
+ const SectionManager = global.SectionManager;
+ const manager = new SectionManager();
+
+ manager.createSectionsFromMarkdown('# Test\nContent');
+ const status = manager.getDocumentStatus();
+
+ runner.expect(status).toHaveProperty('totalSections');
+ runner.expect(status).toHaveProperty('editingSections');
+ runner.expect(status.totalSections).toBe(1);
+ });
+
+ runner.it('should preserve getAllSections functionality', () => {
+ const SectionManager = global.SectionManager;
+ const manager = new SectionManager();
+
+ const testMarkdown = '# One\nContent\n\n# Two\nMore content';
+ manager.createSectionsFromMarkdown(testMarkdown);
+
+ const allSections = manager.getAllSections();
+ runner.expect(Array.isArray(allSections)).toBeTruthy();
+ runner.expect(allSections.length).toBe(2);
+ });
+
+ runner.it('should preserve section splitting functionality', () => {
+ const SectionManager = global.SectionManager;
+ const manager = new SectionManager();
+
+ const sections = manager.createSectionsFromMarkdown('# Original\nContent');
+ const sectionId = sections[0].id;
+
+ const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
+ const newSections = manager.handleSectionSplit(sectionId, newContent);
+
+ runner.expect(Array.isArray(newSections)).toBeTruthy();
+ runner.expect(newSections.length).toBe(2);
+ runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
+ });
+});
+
+// Export API tests for use during extraction
+const SECTION_MANAGER_API_TESTS = [
+ (SectionManager) => {
+ const manager = new SectionManager();
+ if (!manager.sections || !(manager.sections instanceof Map)) {
+ throw new Error('sections property missing or not a Map');
+ }
+ },
+ (SectionManager) => {
+ const manager = new SectionManager();
+ if (typeof manager.createSectionsFromMarkdown !== 'function') {
+ throw new Error('createSectionsFromMarkdown method missing');
+ }
+ },
+ (SectionManager) => {
+ const manager = new SectionManager();
+ if (typeof manager.startEditing !== 'function') {
+ throw new Error('startEditing method missing');
+ }
+ },
+ (SectionManager) => {
+ const manager = new SectionManager();
+ if (typeof manager.stopEditing !== 'function') {
+ throw new Error('stopEditing method missing');
+ }
+ }
+];
+
+module.exports = {
+ runner,
+ EXPECTED_SECTION_MANAGER_API,
+ SECTION_MANAGER_API_TESTS
+};
+
+// Run tests if called directly
+if (require.main === module) {
+ console.log('π§ͺ Testing SectionManager Component Extraction');
+ runner.run().then(() => {
+ console.log('β SectionManager extraction tests completed');
+ });
+}
\ No newline at end of file
diff --git a/testdrive-jsui/static/js/tests/test.md b/testdrive-jsui/static/js/tests/test.md
new file mode 100644
index 00000000..239c58bf
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test.md
@@ -0,0 +1,6 @@
+# Test Document
+
+This is a test document to check if UI controls appear in edit mode.
+
+## Section 1
+Some content here.
diff --git a/testdrive-jsui/static/js/tests/test_edit.html b/testdrive-jsui/static/js/tests/test_edit.html
new file mode 100644
index 00000000..8076d97e
--- /dev/null
+++ b/testdrive-jsui/static/js/tests/test_edit.html
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+ Test Document
+
+
+
+
+
+
+
+
+
+
+
+
+
Test Document
+
This is a test document to check if UI controls appear in edit mode.
+
Section 1
+
Some content here.
+
+
-- html from markdown by MarkiTect on 2025-11-11 23:42:23 by worsch
+
+
+
+
+
+ {js_scripts}
+
+
+
+
+
\ No newline at end of file
diff --git a/testdrive-jsui/test-documents/sample.md b/testdrive-jsui/test-documents/sample.md
new file mode 100644
index 00000000..10658e08
--- /dev/null
+++ b/testdrive-jsui/test-documents/sample.md
@@ -0,0 +1,57 @@
+# TestDrive JSUI Sample Document
+
+This is a sample markdown document for testing the TestDrive JavaScript UI plugin.
+
+## Features to Test
+
+### Basic Editing
+- Click any section to edit it
+- Use the save button to download your changes
+- Reset button restores original content
+
+### Control Panels
+- **Contents Control** (Northwest): Document outline and navigation
+- **Status Control** (East): Current document statistics
+- **Debug Control** (Southeast): Development information and logs
+- **Edit Control** (Northeast): Main editing actions
+
+### Markdown Support
+Test various markdown elements:
+
+**Bold text** and *italic text*
+
+> This is a blockquote
+> with multiple lines
+
+```javascript
+// Code blocks with syntax highlighting
+function testFunction() {
+ console.log("Hello from TestDrive JSUI!");
+ return true;
+}
+```
+
+### Lists
+1. Numbered list item one
+2. Numbered list item two
+3. Numbered list item three
+
+- Bullet list item
+- Another bullet item
+ - Nested bullet item
+ - Another nested item
+
+### Tables
+
+| Feature | Status | Notes |
+|---------|--------|--------|
+| Section editing | β Working | Click to edit |
+| Asset loading | β Working | External scripts |
+| Configuration | β Working | JSON interface |
+| Controls | π§ Testing | Compass positioning |
+
+### Links and Images
+Visit the [Markitect repository](https://github.com/markitect/markitect) for more information.
+
+---
+*Test document for TestDrive JSUI plugin development*
\ No newline at end of file
diff --git a/testdrive-jsui/test.html b/testdrive-jsui/test.html
new file mode 100644
index 00000000..69da30ef
--- /dev/null
+++ b/testdrive-jsui/test.html
@@ -0,0 +1,149 @@
+
+
+
+
+
+ TestDrive JSUI - Standalone Test
+
+
+
+
+
+
+
+
+
+
π§ͺ TestDrive JSUI - Standalone Test Environment
+
This is a standalone test page for developing JavaScript UI components.
+
Development Mode: Assets loaded directly from static/ directory
+
+
+
+
+
TestDrive JSUI Sample Document
+
This is a sample markdown document for testing the TestDrive JavaScript UI plugin.
+
Features to Test
+
Basic Editing
+
+
Click any section to edit it
+
Use the save button to download your changes
+
Reset button restores original content
+
+
Control Panels
+
+
Contents Control (Northwest): Document outline and navigation
+
Status Control (East): Current document statistics
+
Debug Control (Southeast): Development information and logs
+
Edit Control (Northeast): Main editing actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file