import { describe, it, expect, beforeEach, vi } from 'vitest' import { setupBasicDOM } from './setup.js' import { createSampleProject, createSampleCSV, createSampleTemplate, createMalformedTemplate, createLargeDataset, mockFetch, mockPapaParse } from './testHelpers.js' // Import both engine and generator const fs = await import('fs/promises') const engineCode = await fs.readFile('./engine.js', 'utf-8') const generatorCode = await fs.readFile('./generator.js', 'utf-8') describe('Timeline Integration', () => { let timelineEngine, timelineGenerator beforeEach(() => { setupBasicDOM() // Reset global window object global.window = global // Execute both modules eval(generatorCode) eval(engineCode) timelineEngine = global.window.timelineEngine timelineGenerator = global.window.timelineGenerator }) describe('End-to-End Timeline Generation', () => { it('should load project, process CSV, and generate timeline with template-v2', async () => { const config = createSampleProject() const csvData = createSampleCSV() const template = createSampleTemplate() // Mock fetch calls in order: template, CSV mockFetch(template) mockFetch(csvData) mockPapaParse() await timelineEngine.loadProjectConfigObject(config) // Verify project loaded expect(document.getElementById('projectName').textContent).toBe('Test Project') expect(document.getElementById('projectSubtitle').textContent).toBe('A test project for unit testing') // Verify timeline generated with template-v2 format const viewer = document.getElementById('viewer') expect(viewer.innerHTML).toContain(' { const config = createSampleProject() const originalCSV = createSampleCSV() const overrideCSV = 'ID,Title,Lane,Due\nO-1,Override Task,Override Lane,2025-06-01' // First load project with original CSV mockFetch(createSampleTemplate()) mockFetch(originalCSV) global.Papa.parse.mockImplementation((text, options) => { const isOverride = text.includes('Override Task') const mockData = isOverride ? [{ ID: 'O-1', Title: 'Override Task', Lane: 'Override Lane', Due: '2025-06-01' }] : [{ ID: 'T-1', Title: 'First Task', Lane: 'Development', Due: '2025-01-15' }] options.complete({ data: mockData }) }) await timelineEngine.loadProjectConfigObject(config) expect(document.getElementById('viewer').innerHTML).toContain('First Task') // Then override CSV timelineEngine.csvOverride = true timelineEngine.processCsv(overrideCSV) expect(document.getElementById('viewer').innerHTML).toContain('Override Task') }) it('should handle project load failures gracefully', async () => { // Mock fetch failures global.fetch.mockRejectedValue(new Error('Network error')) const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await timelineEngine.autoLoadDefaultProject() // Should not crash, just log messages expect(consoleSpy).toHaveBeenCalled() consoleSpy.mockRestore() }) it('should generate timeline with large dataset (60+ items)', async () => { const config = createSampleProject() const largeItems = createLargeDataset(60) mockFetch(createSampleTemplate()) mockFetch('') // Mock CSV fetch global.Papa.parse.mockImplementation((text, options) => { // Create mock CSV data from large dataset const mockData = largeItems.map((item, idx) => ({ ID: item.id, Title: item.title, Lane: item.lane, Due: item.due.toISOString().split('T')[0] })) options.complete({ data: mockData }) }) await timelineEngine.loadProjectConfigObject(config) const viewer = document.getElementById('viewer') const svg = viewer.innerHTML // Verify SVG contains multiple lanes expect(svg).toContain('Development') expect(svg).toContain('Testing') expect(svg).toContain('Design') expect(svg).toContain('DevOps') expect(svg).toContain('Documentation') // Verify SVG contains task data expect(svg).toContain('TASK-1') expect(svg).toContain('TASK-60') // Verify SVG has reasonable dimensions const widthMatch = svg.match(/width="(\d+)"/) const heightMatch = svg.match(/height="(\d+)"/) expect(parseInt(widthMatch[1])).toBeGreaterThan(500) expect(parseInt(heightMatch[1])).toBeGreaterThan(300) }) it('should handle date range edge cases (24+ months)', async () => { const config = { ...createSampleProject(), settings: { timelineMonths: 30 } } const edgeCaseItems = [ { id: 'EARLY-1', title: 'Very Early Task', lane: 'Dev', due: new Date('2024-01-01') }, { id: 'MID-1', title: 'Middle Task', lane: 'Dev', due: new Date('2025-06-15') }, { id: 'LATE-1', title: 'Future Task', lane: 'Dev', due: new Date('2026-12-31') } ] mockFetch(createSampleTemplate()) mockFetch('') global.Papa.parse.mockImplementation((text, options) => { const mockData = edgeCaseItems.map(item => ({ ID: item.id, Title: item.title, Lane: item.lane, Due: item.due.toISOString().split('T')[0] })) options.complete({ data: mockData }) }) await timelineEngine.loadProjectConfigObject(config) const svg = document.getElementById('viewer').innerHTML // All tasks should be rendered expect(svg).toContain('EARLY-1') expect(svg).toContain('MID-1') expect(svg).toContain('LATE-1') // Should have wide SVG for 30 months const widthMatch = svg.match(/width="(\d+)"/) expect(parseInt(widthMatch[1])).toBeGreaterThan(2000) }) it('should handle special characters in lane names and task titles', async () => { const config = createSampleProject() mockFetch(createSampleTemplate()) mockFetch('') global.Papa.parse.mockImplementation((text, options) => { const mockData = [ { ID: 'T&1', Title: 'Task with & "quotes"', Lane: 'Dev & Test', Due: '2025-01-15' }, { ID: 'T\'2', Title: 'L\'importance de l\'échappement', Lane: 'Développement', Due: '2025-02-01' } ] options.complete({ data: mockData }) }) // Should not throw error when processing special characters await expect(timelineEngine.loadProjectConfigObject(config)).resolves.not.toThrow() const svg = document.getElementById('viewer').innerHTML // Should generate valid SVG with escaped content (basic check) expect(svg).toContain(' { const config = createSampleProject() mockFetch(createSampleTemplate()) mockFetch('') global.Papa.parse.mockImplementation((text, options) => { options.complete({ data: [] }) }) // Should not throw error await expect(timelineEngine.loadProjectConfigObject(config)).resolves.not.toThrow() // Should show message when no valid items found (not crash) const viewer = document.getElementById('viewer').innerHTML expect(viewer).toContain('Keine gültigen Items gefunden') }) it('should reject malformed template-v2 (missing month-template)', async () => { const config = createSampleProject() const malformedTemplate = createMalformedTemplate('month-template') mockFetch(malformedTemplate) mockFetch(createSampleCSV()) mockPapaParse() // Should handle gracefully without crashing await timelineEngine.loadProjectConfigObject(config) const viewer = document.getElementById('viewer').innerHTML // Either shows error message or handles gracefully expect(viewer).toBeTruthy() }) it('should reject malformed template-v2 (missing lane-template)', async () => { const config = createSampleProject() const malformedTemplate = createMalformedTemplate('lane-template') mockFetch(malformedTemplate) mockFetch(createSampleCSV()) mockPapaParse() // Should handle gracefully without crashing await timelineEngine.loadProjectConfigObject(config) const viewer = document.getElementById('viewer').innerHTML expect(viewer).toBeTruthy() }) it('should reject malformed template-v2 (missing item-template)', async () => { const config = createSampleProject() const malformedTemplate = createMalformedTemplate('item-template') mockFetch(malformedTemplate) mockFetch(createSampleCSV()) mockPapaParse() // Should handle gracefully without crashing await timelineEngine.loadProjectConfigObject(config) const viewer = document.getElementById('viewer').innerHTML expect(viewer).toBeTruthy() }) it('should preserve template styling and definitions', async () => { const config = createSampleProject() // Read actual template-v2.svg to test real styling preservation const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8') mockFetch(templateV2) mockFetch(createSampleCSV()) mockPapaParse() await timelineEngine.loadProjectConfigObject(config) const svg = document.getElementById('viewer').innerHTML // Should preserve gradient and filter definitions from template expect(svg).toContain('monthHeaderGrad') expect(svg).toContain('textShadow') expect(svg).toContain('bgGrid') // Should have proper SVG structure expect(svg).toContain('') expect(svg).toContain('') }) }) describe('DOM Event Handling', () => { it('should handle project file upload', async () => { const config = createSampleProject() const projectInput = document.createElement('input') projectInput.id = 'projectInput' document.body.appendChild(projectInput) // Setup event handlers window.setupEventHandlers() // Mock file reading const mockFile = new File([JSON.stringify(config)], 'project.json', { type: 'application/json' }) mockFile.text = vi.fn().mockResolvedValue(JSON.stringify(config)) // Mock the fetch calls that loadProjectConfigObject will make mockFetch(createSampleTemplate()) mockFetch(createSampleCSV()) mockPapaParse() // Simulate file selection Object.defineProperty(projectInput, 'files', { value: [mockFile], writable: false }) // Trigger the event const event = new Event('change') projectInput.dispatchEvent(event) // Wait for async operations await new Promise(resolve => setTimeout(resolve, 0)) expect(document.getElementById('projectName').textContent).toBe('Test Project') }) it('should handle CSV file upload', async () => { const config = createSampleProject() timelineEngine.config = config timelineEngine.template = createSampleTemplate() // Need template for generation const csvInput = document.createElement('input') csvInput.id = 'csvInput' document.body.appendChild(csvInput) // Setup event handlers window.setupEventHandlers() const csvContent = createSampleCSV() const mockFile = new File([csvContent], 'data.csv', { type: 'text/csv' }) mockFile.text = vi.fn().mockResolvedValue(csvContent) global.Papa.parse.mockImplementation((text, options) => { options.complete({ data: [{ ID: 'T-1', Title: 'Uploaded Task', Lane: 'Upload Lane', Due: '2025-01-15' }] }) }) Object.defineProperty(csvInput, 'files', { value: [mockFile], writable: false }) const event = new Event('change') csvInput.dispatchEvent(event) await new Promise(resolve => setTimeout(resolve, 0)) expect(timelineEngine.csvOverride).toBe(true) expect(document.getElementById('viewer').innerHTML).toContain('Uploaded Task') }) }) describe('SVG Export', () => { it('should hide IDs in external view for export', async () => { // Setup timeline with items const config = createSampleProject() mockFetch(createSampleTemplate()) mockFetch(createSampleCSV()) global.Papa.parse.mockImplementation((text, options) => { options.complete({ data: [{ ID: 'T-1', Title: 'Export Task', Lane: 'Export Lane', Due: '2025-01-15' }] }) }) await timelineEngine.loadProjectConfigObject(config) // Mock the download functionality const mockCreateElement = vi.spyOn(document, 'createElement').mockImplementation((tagName) => { if (tagName === 'a') { return { href: '', download: '', click: vi.fn() } } return document.createElement(tagName) }) global.URL.createObjectURL = vi.fn().mockReturnValue('blob:url') global.URL.revokeObjectURL = vi.fn() const downloadBtn = document.getElementById('downloadSvg') downloadBtn.click() // Verify that item-id elements would be hidden const svg = document.querySelector('#viewer svg') expect(svg).toBeTruthy() mockCreateElement.mockRestore() }) it('should generate downloadable SVG with proper dimensions', async () => { const config = createSampleProject() mockFetch(createSampleTemplate()) mockFetch(createSampleCSV()) mockPapaParse() await timelineEngine.loadProjectConfigObject(config) const svg = document.querySelector('#viewer svg') // Verify SVG has width and height attributes expect(svg.getAttribute('width')).toBeTruthy() expect(svg.getAttribute('height')).toBeTruthy() expect(svg.getAttribute('viewBox')).toBeTruthy() // Verify viewBox matches dimensions const width = svg.getAttribute('width') const height = svg.getAttribute('height') const viewBox = svg.getAttribute('viewBox') expect(viewBox).toContain(width) expect(viewBox).toContain(height) }) }) })