generated from coulomb/repo-seed
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>
381 lines
13 KiB
JavaScript
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)
|
|
})
|
|
})
|
|
})
|