/** * TDD Test Suite for DocumentNavigator Widget * * Tests the Substack-style floating navigation widget for document headings. * Following TDD methodology: write tests first, then implement functionality. */ // Simple test runner for browser environment class DocumentNavigatorTestRunner { constructor() { this.tests = []; this.results = { passed: 0, failed: 0, total: 0 }; } test(name, testFn) { this.tests.push({ name, testFn }); } expect(actual) { return { toBe: (expected) => { if (actual !== expected) { throw new Error(`Expected ${actual} to be ${expected}`); } }, toBeInstanceOf: (expectedClass) => { if (!(actual instanceof expectedClass)) { throw new Error(`Expected ${actual} to be instance of ${expectedClass.name}`); } }, toBeTruthy: () => { if (!actual) { throw new Error(`Expected ${actual} to be truthy`); } }, toBeFalsy: () => { if (actual) { throw new Error(`Expected ${actual} to be falsy`); } }, toContain: (expected) => { if (typeof actual === 'string' && !actual.includes(expected)) { throw new Error(`Expected "${actual}" to contain "${expected}"`); } if (Array.isArray(actual) && !actual.includes(expected)) { throw new Error(`Expected array to contain ${expected}`); } }, toHaveLength: (expected) => { if (actual.length !== expected) { throw new Error(`Expected length ${actual.length} to be ${expected}`); } }, toBeGreaterThan: (expected) => { if (actual <= expected) { throw new Error(`Expected ${actual} to be greater than ${expected}`); } } }; } async run() { console.log('๐Ÿงช Running DocumentNavigator TDD Test Suite...\n'); for (const { name, testFn } of this.tests) { this.results.total++; try { await testFn.call(this); this.results.passed++; console.log(`โœ… ${name}`); } catch (error) { this.results.failed++; console.log(`โŒ ${name}`); console.log(` ${error.message}\n`); } } this.printSummary(); } printSummary() { console.log(`\n๐Ÿ“Š Test Results:`); console.log(` Passed: ${this.results.passed}`); console.log(` Failed: ${this.results.failed}`); console.log(` Total: ${this.results.total}`); if (this.results.failed === 0) { console.log(`\n๐ŸŽ‰ All tests passed!`); } else { console.log(`\nโŒ ${this.results.failed} test(s) failed.`); } } } // Create test runner const runner = new DocumentNavigatorTestRunner(); // Test Suite: DocumentNavigator Widget runner.test('DocumentNavigator class should exist and be importable', async function() { // This test will fail initially - we haven't created the class yet try { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); this.expect(DocumentNavigator).toBeTruthy(); this.expect(typeof DocumentNavigator).toBe('function'); } catch (error) { throw new Error(`DocumentNavigator class not found: ${error.message}`); } }); runner.test('DocumentNavigator should extend UIWidget', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const { UIWidget } = await import('../widgets/base/UIWidget.js'); const navigator = new DocumentNavigator(); this.expect(navigator).toBeInstanceOf(UIWidget); }); runner.test('DocumentNavigator should initialize with default configuration', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const navigator = new DocumentNavigator(); // Test default configuration this.expect(navigator.config.position).toBe('left'); this.expect(navigator.config.collapsed).toBe(true); this.expect(navigator.config.autoHide).toBe(true); this.expect(navigator.config.maxHeadingLevel).toBe(3); this.expect(navigator.config.enableScrollSpy).toBe(true); }); runner.test('DocumentNavigator should accept custom configuration', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const customConfig = { position: 'right', collapsed: false, maxHeadingLevel: 4, theme: 'dark' }; const navigator = new DocumentNavigator(customConfig); this.expect(navigator.config.position).toBe('right'); this.expect(navigator.config.collapsed).toBe(false); this.expect(navigator.config.maxHeadingLevel).toBe(4); this.expect(navigator.config.theme).toBe('dark'); }); runner.test('DocumentNavigator should render floating panel element', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const navigator = new DocumentNavigator(); await navigator.render(); this.expect(navigator.element).toBeInstanceOf(HTMLElement); this.expect(navigator.element.classList.contains('document-navigator')).toBeTruthy(); this.expect(navigator.element.style.position).toBe('fixed'); }); runner.test('DocumentNavigator should have toggle button in collapsed state', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const navigator = new DocumentNavigator({ collapsed: true }); await navigator.render(); const toggleButton = navigator.findElement('.navigator-toggle'); this.expect(toggleButton).toBeInstanceOf(HTMLElement); this.expect(toggleButton.style.display).not.toBe('none'); const navList = navigator.findElement('.navigator-list'); this.expect(navList.style.display).toBe('none'); }); runner.test('DocumentNavigator should extract headings from document', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); // Create test document with headings const testContainer = document.createElement('div'); testContainer.innerHTML = `

First Heading

Some content

Second Heading

Third Heading

More content

Fourth Heading

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

Chapter 1

Section 1.1

Subsection 1.1.1

Subsection 1.1.2

Section 1.2

Chapter 2

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

Target Heading

Spacer content

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

Section 1

Section 2

Section 3

`; document.body.appendChild(testContainer); const navigator = new DocumentNavigator({ container: testContainer, enableScrollSpy: true }); await navigator.render(); // Test current section detection const currentSection = navigator.getCurrentSection(); this.expect(currentSection).toBeTruthy(); // Cleanup document.body.removeChild(testContainer); }); runner.test('DocumentNavigator should handle responsive behavior', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const navigator = new DocumentNavigator({ autoHide: true }); await navigator.render(); // Mock viewport resize const originalInnerWidth = window.innerWidth; // Test mobile viewport Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true }); navigator.handleResize(); this.expect(navigator.element.style.display).toBe('none'); // Test desktop viewport Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true }); navigator.handleResize(); this.expect(navigator.element.style.display).not.toBe('none'); // Restore original Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true }); }); runner.test('DocumentNavigator should provide keyboard navigation support', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const navigator = new DocumentNavigator(); await navigator.render(); // Test keyboard shortcuts let expandCalled = false; let collapseCalled = false; navigator.expand = async () => { expandCalled = true; }; navigator.collapse = async () => { collapseCalled = true; }; // Simulate keyboard events const element = navigator.element; // Test Escape key (should collapse) const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }); element.dispatchEvent(escapeEvent); this.expect(collapseCalled).toBeTruthy(); // Test Enter/Space key (should expand) const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); element.dispatchEvent(enterEvent); this.expect(expandCalled).toBeTruthy(); }); runner.test('DocumentNavigator should emit events for user interactions', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); const navigator = new DocumentNavigator(); await navigator.render(); // Test event emission let navigationEvent = null; navigator.addEventListener('navigate', (e) => { navigationEvent = e; }); let toggleEvent = null; navigator.addEventListener('toggle', (e) => { toggleEvent = e; }); // Trigger navigation navigator.navigateToHeading('test-heading'); this.expect(navigationEvent).toBeTruthy(); this.expect(navigationEvent.detail.target).toBe('test-heading'); // Trigger toggle await navigator.toggle(); this.expect(toggleEvent).toBeTruthy(); }); runner.test('DocumentNavigator should handle empty document gracefully', async function() { const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); // Create empty container const emptyContainer = document.createElement('div'); document.body.appendChild(emptyContainer); const navigator = new DocumentNavigator({ container: emptyContainer }); const headings = navigator.extractHeadings(); this.expect(headings).toHaveLength(0); await navigator.render(); const navList = navigator.findElement('.navigator-list'); this.expect(navList.children).toHaveLength(0); // Should show empty state message const emptyMessage = navigator.findElement('.navigator-empty'); this.expect(emptyMessage).toBeTruthy(); // Cleanup document.body.removeChild(emptyContainer); }); // Export test runner for use in HTML window.runDocumentNavigatorTests = () => runner.run(); console.log('๐Ÿ“‹ DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()'); export { runner };