generated from coulomb/repo-seed
- Add Vitest + jsdom testing framework - Create unit tests for engine.js and generator.js - Add integration tests for end-to-end workflows - Include test utilities and setup helpers - Document testing approach in TESTING.md - Document all dependencies in DEPENDS.md - Add Makefile with test targets and dev workflow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
246 lines
8.2 KiB
JavaScript
246 lines
8.2 KiB
JavaScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { setupBasicDOM } from './setup.js'
|
|
import { createSampleProject, createSampleCSV, createSampleTemplate, mockFetch } 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', async () => {
|
|
const config = createSampleProject()
|
|
const csvData = createSampleCSV()
|
|
const template = createSampleTemplate()
|
|
|
|
// Mock fetch calls in order: template, CSV
|
|
mockFetch(template)
|
|
mockFetch(csvData)
|
|
|
|
// Mock Papa.parse to process the CSV
|
|
global.Papa.parse.mockImplementation((text, options) => {
|
|
const lines = text.trim().split('\n')
|
|
const headers = lines[0].split(',')
|
|
const data = lines.slice(1).map(line => {
|
|
const values = line.split(',')
|
|
const obj = {}
|
|
headers.forEach((header, i) => {
|
|
obj[header] = values[i]
|
|
})
|
|
return obj
|
|
})
|
|
options.complete({ data })
|
|
})
|
|
|
|
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
|
|
const viewer = document.getElementById('viewer')
|
|
expect(viewer.innerHTML).toContain('<svg')
|
|
expect(viewer.innerHTML).toContain('First Task')
|
|
expect(viewer.innerHTML).toContain('Development')
|
|
|
|
// 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(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 template with custom macros', async () => {
|
|
const config = createSampleProject()
|
|
const customTemplate = `<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
|
|
<g class="timeline-months">{{MONTHS}}</g>
|
|
<g class="timeline-lanes">{{LANES}}</g>
|
|
</svg>`
|
|
|
|
mockFetch(customTemplate)
|
|
mockFetch(createSampleCSV())
|
|
|
|
global.Papa.parse.mockImplementation((text, options) => {
|
|
options.complete({
|
|
data: [{ ID: 'T-1', Title: 'Test Task', Lane: 'Test Lane', Due: '2025-01-15' }]
|
|
})
|
|
})
|
|
|
|
await timelineEngine.loadProjectConfigObject(config)
|
|
|
|
const viewer = document.getElementById('viewer')
|
|
const svg = viewer.innerHTML
|
|
|
|
expect(svg).toContain('<g class="timeline-months">')
|
|
expect(svg).toContain('<g class="timeline-lanes">')
|
|
expect(svg).not.toContain('{{MONTHS}}')
|
|
expect(svg).not.toContain('{{LANES}}')
|
|
})
|
|
|
|
it('should handle project load failures gracefully', async () => {
|
|
// Mock fetch failures
|
|
global.fetch.mockRejectedValue(new Error('Network error'))
|
|
|
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
await timelineEngine.autoLoadDefaultProject()
|
|
|
|
// Should not crash, just log warnings
|
|
expect(consoleSpy).toHaveBeenCalled()
|
|
|
|
consoleSpy.mockRestore()
|
|
})
|
|
})
|
|
|
|
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)
|
|
|
|
// 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())
|
|
|
|
global.Papa.parse.mockImplementation((text, options) => {
|
|
options.complete({ data: [] })
|
|
})
|
|
|
|
// 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
|
|
|
|
const csvInput = document.createElement('input')
|
|
csvInput.id = 'csvInput'
|
|
document.body.appendChild(csvInput)
|
|
|
|
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()
|
|
})
|
|
})
|
|
}) |