Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
- Add comprehensive widget plugin infrastructure documentation and workplan - Include complete DocumentNavigator integration documentation - Add TDD test suite with 15 comprehensive test cases for DocumentNavigator - Include widget base classes (Widget, UIWidget) for future development - Add DocumentNavigator plugin definition following planned architecture - Include test runner and demo pages for development validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
432 lines
15 KiB
JavaScript
432 lines
15 KiB
JavaScript
/**
|
|
* TDD Test Suite for DocumentNavigator Widget
|
|
*
|
|
* Tests the Substack-style floating navigation widget for document headings.
|
|
* Following TDD methodology: write tests first, then implement functionality.
|
|
*/
|
|
|
|
// Simple test runner for browser environment
|
|
class DocumentNavigatorTestRunner {
|
|
constructor() {
|
|
this.tests = [];
|
|
this.results = {
|
|
passed: 0,
|
|
failed: 0,
|
|
total: 0
|
|
};
|
|
}
|
|
|
|
test(name, testFn) {
|
|
this.tests.push({ name, testFn });
|
|
}
|
|
|
|
expect(actual) {
|
|
return {
|
|
toBe: (expected) => {
|
|
if (actual !== expected) {
|
|
throw new Error(`Expected ${actual} to be ${expected}`);
|
|
}
|
|
},
|
|
toBeInstanceOf: (expectedClass) => {
|
|
if (!(actual instanceof expectedClass)) {
|
|
throw new Error(`Expected ${actual} to be instance of ${expectedClass.name}`);
|
|
}
|
|
},
|
|
toBeTruthy: () => {
|
|
if (!actual) {
|
|
throw new Error(`Expected ${actual} to be truthy`);
|
|
}
|
|
},
|
|
toBeFalsy: () => {
|
|
if (actual) {
|
|
throw new Error(`Expected ${actual} to be falsy`);
|
|
}
|
|
},
|
|
toContain: (expected) => {
|
|
if (typeof actual === 'string' && !actual.includes(expected)) {
|
|
throw new Error(`Expected "${actual}" to contain "${expected}"`);
|
|
}
|
|
if (Array.isArray(actual) && !actual.includes(expected)) {
|
|
throw new Error(`Expected array to contain ${expected}`);
|
|
}
|
|
},
|
|
toHaveLength: (expected) => {
|
|
if (actual.length !== expected) {
|
|
throw new Error(`Expected length ${actual.length} to be ${expected}`);
|
|
}
|
|
},
|
|
toBeGreaterThan: (expected) => {
|
|
if (actual <= expected) {
|
|
throw new Error(`Expected ${actual} to be greater than ${expected}`);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
async run() {
|
|
console.log('🧪 Running DocumentNavigator TDD Test Suite...\n');
|
|
|
|
for (const { name, testFn } of this.tests) {
|
|
this.results.total++;
|
|
|
|
try {
|
|
await testFn.call(this);
|
|
this.results.passed++;
|
|
console.log(`✅ ${name}`);
|
|
} catch (error) {
|
|
this.results.failed++;
|
|
console.log(`❌ ${name}`);
|
|
console.log(` ${error.message}\n`);
|
|
}
|
|
}
|
|
|
|
this.printSummary();
|
|
}
|
|
|
|
printSummary() {
|
|
console.log(`\n📊 Test Results:`);
|
|
console.log(` Passed: ${this.results.passed}`);
|
|
console.log(` Failed: ${this.results.failed}`);
|
|
console.log(` Total: ${this.results.total}`);
|
|
|
|
if (this.results.failed === 0) {
|
|
console.log(`\n🎉 All tests passed!`);
|
|
} else {
|
|
console.log(`\n❌ ${this.results.failed} test(s) failed.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create test runner
|
|
const runner = new DocumentNavigatorTestRunner();
|
|
|
|
// Test Suite: DocumentNavigator Widget
|
|
runner.test('DocumentNavigator class should exist and be importable', async function() {
|
|
// This test will fail initially - we haven't created the class yet
|
|
try {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
this.expect(DocumentNavigator).toBeTruthy();
|
|
this.expect(typeof DocumentNavigator).toBe('function');
|
|
} catch (error) {
|
|
throw new Error(`DocumentNavigator class not found: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
runner.test('DocumentNavigator should extend UIWidget', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
const { UIWidget } = await import('../widgets/base/UIWidget.js');
|
|
|
|
const navigator = new DocumentNavigator();
|
|
this.expect(navigator).toBeInstanceOf(UIWidget);
|
|
});
|
|
|
|
runner.test('DocumentNavigator should initialize with default configuration', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const navigator = new DocumentNavigator();
|
|
|
|
// Test default configuration
|
|
this.expect(navigator.config.position).toBe('left');
|
|
this.expect(navigator.config.collapsed).toBe(true);
|
|
this.expect(navigator.config.autoHide).toBe(true);
|
|
this.expect(navigator.config.maxHeadingLevel).toBe(3);
|
|
this.expect(navigator.config.enableScrollSpy).toBe(true);
|
|
});
|
|
|
|
runner.test('DocumentNavigator should accept custom configuration', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const customConfig = {
|
|
position: 'right',
|
|
collapsed: false,
|
|
maxHeadingLevel: 4,
|
|
theme: 'dark'
|
|
};
|
|
|
|
const navigator = new DocumentNavigator(customConfig);
|
|
|
|
this.expect(navigator.config.position).toBe('right');
|
|
this.expect(navigator.config.collapsed).toBe(false);
|
|
this.expect(navigator.config.maxHeadingLevel).toBe(4);
|
|
this.expect(navigator.config.theme).toBe('dark');
|
|
});
|
|
|
|
runner.test('DocumentNavigator should render floating panel element', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const navigator = new DocumentNavigator();
|
|
await navigator.render();
|
|
|
|
this.expect(navigator.element).toBeInstanceOf(HTMLElement);
|
|
this.expect(navigator.element.classList.contains('document-navigator')).toBeTruthy();
|
|
this.expect(navigator.element.style.position).toBe('fixed');
|
|
});
|
|
|
|
runner.test('DocumentNavigator should have toggle button in collapsed state', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const navigator = new DocumentNavigator({ collapsed: true });
|
|
await navigator.render();
|
|
|
|
const toggleButton = navigator.findElement('.navigator-toggle');
|
|
this.expect(toggleButton).toBeInstanceOf(HTMLElement);
|
|
this.expect(toggleButton.style.display).not.toBe('none');
|
|
|
|
const navList = navigator.findElement('.navigator-list');
|
|
this.expect(navList.style.display).toBe('none');
|
|
});
|
|
|
|
runner.test('DocumentNavigator should extract headings from document', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
// Create test document with headings
|
|
const testContainer = document.createElement('div');
|
|
testContainer.innerHTML = `
|
|
<h1 id="heading1">First Heading</h1>
|
|
<p>Some content</p>
|
|
<h2 id="heading2">Second Heading</h2>
|
|
<h3 id="heading3">Third Heading</h3>
|
|
<p>More content</p>
|
|
<h2 id="heading4">Fourth Heading</h2>
|
|
`;
|
|
document.body.appendChild(testContainer);
|
|
|
|
const navigator = new DocumentNavigator({
|
|
container: testContainer,
|
|
maxHeadingLevel: 3
|
|
});
|
|
|
|
const headings = navigator.extractHeadings();
|
|
|
|
this.expect(headings).toHaveLength(4);
|
|
this.expect(headings[0].tagName).toBe('H1');
|
|
this.expect(headings[0].textContent).toBe('First Heading');
|
|
this.expect(headings[1].tagName).toBe('H2');
|
|
this.expect(headings[2].tagName).toBe('H3');
|
|
this.expect(headings[3].tagName).toBe('H2');
|
|
|
|
// Cleanup
|
|
document.body.removeChild(testContainer);
|
|
});
|
|
|
|
runner.test('DocumentNavigator should build navigation hierarchy', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
// Create test document with nested headings
|
|
const testContainer = document.createElement('div');
|
|
testContainer.innerHTML = `
|
|
<h1>Chapter 1</h1>
|
|
<h2>Section 1.1</h2>
|
|
<h3>Subsection 1.1.1</h3>
|
|
<h3>Subsection 1.1.2</h3>
|
|
<h2>Section 1.2</h2>
|
|
<h1>Chapter 2</h1>
|
|
`;
|
|
document.body.appendChild(testContainer);
|
|
|
|
const navigator = new DocumentNavigator({ container: testContainer });
|
|
await navigator.render();
|
|
|
|
const navItems = navigator.buildNavigationTree();
|
|
|
|
// Should have hierarchical structure
|
|
this.expect(navItems).toHaveLength(2); // 2 H1 elements
|
|
this.expect(navItems[0].children).toHaveLength(2); // 2 H2 under first H1
|
|
this.expect(navItems[0].children[0].children).toHaveLength(2); // 2 H3 under first H2
|
|
|
|
// Cleanup
|
|
document.body.removeChild(testContainer);
|
|
});
|
|
|
|
runner.test('DocumentNavigator should handle click navigation', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
// Create test document
|
|
const testContainer = document.createElement('div');
|
|
testContainer.innerHTML = `
|
|
<h1 id="target-heading">Target Heading</h1>
|
|
<p style="height: 1000px;">Spacer content</p>
|
|
`;
|
|
document.body.appendChild(testContainer);
|
|
|
|
const navigator = new DocumentNavigator({ container: testContainer });
|
|
await navigator.render();
|
|
|
|
// Simulate click on navigation item
|
|
const navItem = navigator.findElement('[data-target="target-heading"]');
|
|
this.expect(navItem).toBeTruthy();
|
|
|
|
// Mock scrollIntoView for testing
|
|
const targetElement = document.getElementById('target-heading');
|
|
let scrollCalled = false;
|
|
targetElement.scrollIntoView = () => { scrollCalled = true; };
|
|
|
|
// Click navigation item
|
|
navItem.click();
|
|
|
|
this.expect(scrollCalled).toBeTruthy();
|
|
|
|
// Cleanup
|
|
document.body.removeChild(testContainer);
|
|
});
|
|
|
|
runner.test('DocumentNavigator should support expand/collapse functionality', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const navigator = new DocumentNavigator({ collapsed: true });
|
|
await navigator.render();
|
|
|
|
// Should start collapsed
|
|
this.expect(navigator.isCollapsed).toBeTruthy();
|
|
|
|
const toggleButton = navigator.findElement('.navigator-toggle');
|
|
const navList = navigator.findElement('.navigator-list');
|
|
|
|
// Toggle to expanded
|
|
await navigator.expand();
|
|
this.expect(navigator.isCollapsed).toBeFalsy();
|
|
this.expect(navList.style.display).not.toBe('none');
|
|
|
|
// Toggle back to collapsed
|
|
await navigator.collapse();
|
|
this.expect(navigator.isCollapsed).toBeTruthy();
|
|
this.expect(navList.style.display).toBe('none');
|
|
});
|
|
|
|
runner.test('DocumentNavigator should implement scroll spy functionality', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
// Create test document with multiple sections
|
|
const testContainer = document.createElement('div');
|
|
testContainer.innerHTML = `
|
|
<div style="height: 100px;"></div>
|
|
<h1 id="section1">Section 1</h1>
|
|
<div style="height: 400px;"></div>
|
|
<h2 id="section2">Section 2</h2>
|
|
<div style="height: 400px;"></div>
|
|
<h2 id="section3">Section 3</h2>
|
|
<div style="height: 400px;"></div>
|
|
`;
|
|
document.body.appendChild(testContainer);
|
|
|
|
const navigator = new DocumentNavigator({
|
|
container: testContainer,
|
|
enableScrollSpy: true
|
|
});
|
|
await navigator.render();
|
|
|
|
// Test current section detection
|
|
const currentSection = navigator.getCurrentSection();
|
|
this.expect(currentSection).toBeTruthy();
|
|
|
|
// Cleanup
|
|
document.body.removeChild(testContainer);
|
|
});
|
|
|
|
runner.test('DocumentNavigator should handle responsive behavior', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const navigator = new DocumentNavigator({ autoHide: true });
|
|
await navigator.render();
|
|
|
|
// Mock viewport resize
|
|
const originalInnerWidth = window.innerWidth;
|
|
|
|
// Test mobile viewport
|
|
Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true });
|
|
navigator.handleResize();
|
|
this.expect(navigator.element.style.display).toBe('none');
|
|
|
|
// Test desktop viewport
|
|
Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true });
|
|
navigator.handleResize();
|
|
this.expect(navigator.element.style.display).not.toBe('none');
|
|
|
|
// Restore original
|
|
Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true });
|
|
});
|
|
|
|
runner.test('DocumentNavigator should provide keyboard navigation support', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const navigator = new DocumentNavigator();
|
|
await navigator.render();
|
|
|
|
// Test keyboard shortcuts
|
|
let expandCalled = false;
|
|
let collapseCalled = false;
|
|
|
|
navigator.expand = async () => { expandCalled = true; };
|
|
navigator.collapse = async () => { collapseCalled = true; };
|
|
|
|
// Simulate keyboard events
|
|
const element = navigator.element;
|
|
|
|
// Test Escape key (should collapse)
|
|
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
element.dispatchEvent(escapeEvent);
|
|
this.expect(collapseCalled).toBeTruthy();
|
|
|
|
// Test Enter/Space key (should expand)
|
|
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
element.dispatchEvent(enterEvent);
|
|
this.expect(expandCalled).toBeTruthy();
|
|
});
|
|
|
|
runner.test('DocumentNavigator should emit events for user interactions', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
const navigator = new DocumentNavigator();
|
|
await navigator.render();
|
|
|
|
// Test event emission
|
|
let navigationEvent = null;
|
|
navigator.addEventListener('navigate', (e) => {
|
|
navigationEvent = e;
|
|
});
|
|
|
|
let toggleEvent = null;
|
|
navigator.addEventListener('toggle', (e) => {
|
|
toggleEvent = e;
|
|
});
|
|
|
|
// Trigger navigation
|
|
navigator.navigateToHeading('test-heading');
|
|
this.expect(navigationEvent).toBeTruthy();
|
|
this.expect(navigationEvent.detail.target).toBe('test-heading');
|
|
|
|
// Trigger toggle
|
|
await navigator.toggle();
|
|
this.expect(toggleEvent).toBeTruthy();
|
|
});
|
|
|
|
runner.test('DocumentNavigator should handle empty document gracefully', async function() {
|
|
const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js');
|
|
|
|
// Create empty container
|
|
const emptyContainer = document.createElement('div');
|
|
document.body.appendChild(emptyContainer);
|
|
|
|
const navigator = new DocumentNavigator({ container: emptyContainer });
|
|
|
|
const headings = navigator.extractHeadings();
|
|
this.expect(headings).toHaveLength(0);
|
|
|
|
await navigator.render();
|
|
const navList = navigator.findElement('.navigator-list');
|
|
this.expect(navList.children).toHaveLength(0);
|
|
|
|
// Should show empty state message
|
|
const emptyMessage = navigator.findElement('.navigator-empty');
|
|
this.expect(emptyMessage).toBeTruthy();
|
|
|
|
// Cleanup
|
|
document.body.removeChild(emptyContainer);
|
|
});
|
|
|
|
// Export test runner for use in HTML
|
|
window.runDocumentNavigatorTests = () => runner.run();
|
|
|
|
console.log('📋 DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()');
|
|
|
|
export { runner }; |