Files
timeline-svg/test/integration.test.js
tegwick a5811040e7 fix: initialize file editor and remove broken individual file uploads
Changes:
- Add fileEditor.init() call in DOMContentLoaded to activate edit buttons
- Remove individual file upload inputs (projectInput, svgInput, cssInput, csvInput)
  that had CORS issues when loading project configurations
- Keep only the folder picker which works reliably
- Update UI to emphasize folder picker as the primary loading method
- Remove corresponding event handlers from engine.js
- Remove tests for individual file upload functionality

The folder picker loads all project files in one operation without CORS
issues, while individual file uploads failed when trying to load
referenced files (CSV, SVG, CSS) from the project.json.

All 56 tests passing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 23:14:57 +01:00

381 lines
13 KiB
JavaScript

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: CSS, SVG template, CSV
mockFetch('/* test css */') // CSS
mockFetch(template) // SVG
mockFetch(csvData) // CSV
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('<svg')
expect(viewer.innerHTML).toContain('First Task')
expect(viewer.innerHTML).toContain('Development')
// Verify no template placeholders remain
expect(viewer.innerHTML).not.toContain('{{MONTH_X}}')
expect(viewer.innerHTML).not.toContain('{{LANE_Y}}')
expect(viewer.innerHTML).not.toContain('{{ITEM_X}}')
// Verify download button enabled
const downloadBtn = document.getElementById('downloadSvg')
expect(downloadBtn.disabled).toBe(false)
expect(downloadBtn.style.opacity).toBe('1')
})
it('should handle CSV override correctly', async () => {
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('/* test css */') // CSS
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('/* test css */') // CSS
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('/* test css */') // CSS
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('/* test css */') // CSS
mockFetch(createSampleTemplate())
mockFetch('')
global.Papa.parse.mockImplementation((text, options) => {
const mockData = [
{ ID: 'T&1', Title: 'Task with <special> & "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('<svg')
expect(svg).toContain('viewBox')
// Characters are XML-escaped by escapeXml function (tested in generator.test.js)
})
it('should handle empty CSV gracefully', async () => {
const config = createSampleProject()
mockFetch('/* test css */') // CSS
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('/* test css */') // CSS
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('/* test css */') // CSS
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('/* test css */') // CSS
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('/* test css */') // CSS
mockFetch(templateV2)
mockFetch(createSampleCSV())
mockPapaParse()
await timelineEngine.loadProjectConfigObject(config)
const svg = document.getElementById('viewer').innerHTML
// Template elements are removed during generation (generator.js:202-205)
// Instead verify that the template was successfully used to generate content
expect(svg).toContain('<svg')
expect(svg).toContain('viewBox')
// Should have generated content from the template
expect(svg).toContain('First Task') // Item from CSV
expect(svg).toContain('Development') // Lane from CSV
// Should have <defs> section (for gradients, filters, etc. but not template elements)
expect(svg).toContain('<defs')
})
})
// DOM Event Handling tests removed - individual file uploads replaced with folder picker
describe('SVG Export', () => {
it('should hide IDs in external view for export', async () => {
// Setup timeline with items
const config = createSampleProject()
mockFetch('/* test css */') // CSS
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('/* test css */') // CSS
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)
})
})
})