/** * 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 = `
Some content
More content
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 = `