generated from coulomb/repo-seed
add: comprehensive TDD test infrastructure
- 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>
This commit is contained in:
246
test/integration.test.js
Normal file
246
test/integration.test.js
Normal file
@@ -0,0 +1,246 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user