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>
167 lines
6.0 KiB
JavaScript
167 lines
6.0 KiB
JavaScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { createSampleItems, createSampleProject, createSampleTemplate } from './testHelpers.js'
|
|
|
|
// Import generator by loading it as text and evaluating
|
|
const fs = await import('fs/promises')
|
|
const generatorCode = await fs.readFile('./generator.js', 'utf-8')
|
|
|
|
describe('Timeline Generator', () => {
|
|
let timelineGenerator
|
|
|
|
beforeEach(() => {
|
|
// Reset global window object
|
|
global.window = global
|
|
|
|
// Execute generator code
|
|
eval(generatorCode)
|
|
timelineGenerator = global.window.timelineGenerator
|
|
})
|
|
|
|
describe('escapeXml', () => {
|
|
it('should escape XML special characters', () => {
|
|
const input = '<test & "quotes" & \'apostrophes\'>'
|
|
const expected = '<test & "quotes" & 'apostrophes'>'
|
|
expect(timelineGenerator.escapeXml(input)).toBe(expected)
|
|
})
|
|
|
|
it('should handle empty and null values', () => {
|
|
expect(timelineGenerator.escapeXml('')).toBe('')
|
|
expect(timelineGenerator.escapeXml(null)).toBe('null')
|
|
expect(timelineGenerator.escapeXml(undefined)).toBe('undefined')
|
|
})
|
|
|
|
it('should convert non-strings to strings', () => {
|
|
expect(timelineGenerator.escapeXml(123)).toBe('123')
|
|
expect(timelineGenerator.escapeXml(true)).toBe('true')
|
|
})
|
|
})
|
|
|
|
describe('generate', () => {
|
|
let items, config
|
|
|
|
beforeEach(() => {
|
|
items = createSampleItems()
|
|
config = createSampleProject()
|
|
})
|
|
|
|
it('should generate SVG with template placeholders', () => {
|
|
const template = createSampleTemplate()
|
|
const result = timelineGenerator.generate(items, config, template)
|
|
|
|
expect(result).toContain('<svg xmlns="http://www.w3.org/2000/svg">')
|
|
expect(result).toContain('<rect width="100%" height="100%" fill="#FFFFFF"/>')
|
|
expect(result).not.toContain('{{MONTHS}}')
|
|
expect(result).not.toContain('{{LANES}}')
|
|
})
|
|
|
|
it('should generate fallback SVG when no template provided', () => {
|
|
const result = timelineGenerator.generate(items, config, null)
|
|
|
|
expect(result).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
|
|
expect(result).toContain('width=')
|
|
expect(result).toContain('height=')
|
|
expect(result).toContain('<rect width="100%" height="100%" fill="#FFFFFF"')
|
|
})
|
|
|
|
it('should create month labels and grid lines', () => {
|
|
const result = timelineGenerator.generate(items, config, null)
|
|
|
|
expect(result).toContain('<line')
|
|
expect(result).toContain('stroke="#E3E8EF"')
|
|
expect(result).toContain('<text')
|
|
expect(result).toContain('fill="#5C6B7A"')
|
|
})
|
|
|
|
it('should create lane backgrounds and labels', () => {
|
|
const result = timelineGenerator.generate(items, config, null)
|
|
|
|
expect(result).toContain('Development')
|
|
expect(result).toContain('Testing')
|
|
expect(result).toContain('<rect')
|
|
expect(result).toContain('fill="#FFFFFF"')
|
|
})
|
|
|
|
it('should position items correctly in lanes', () => {
|
|
const result = timelineGenerator.generate(items, config, null)
|
|
|
|
expect(result).toContain('<circle')
|
|
expect(result).toContain('fill="#0A4D8C"')
|
|
expect(result).toContain('T-1')
|
|
expect(result).toContain('First Task')
|
|
})
|
|
|
|
it('should sort items by due date within lanes', () => {
|
|
// Add items with same lane but different dates
|
|
const unsortedItems = [
|
|
{ id: 'T-3', title: 'Third', lane: 'Dev', due: new Date('2025-03-01') },
|
|
{ id: 'T-1', title: 'First', lane: 'Dev', due: new Date('2025-01-01') },
|
|
{ id: 'T-2', title: 'Second', lane: 'Dev', due: new Date('2025-02-01') }
|
|
]
|
|
|
|
const result = timelineGenerator.generate(unsortedItems, config, null)
|
|
const firstIndex = result.indexOf('First')
|
|
const secondIndex = result.indexOf('Second')
|
|
const thirdIndex = result.indexOf('Third')
|
|
|
|
expect(firstIndex).toBeLessThan(secondIndex)
|
|
expect(secondIndex).toBeLessThan(thirdIndex)
|
|
})
|
|
|
|
it('should handle items without lanes', () => {
|
|
const itemsNoLane = [
|
|
{ id: 'T-1', title: 'No Lane Task', lane: null, due: new Date('2025-01-01') }
|
|
]
|
|
|
|
const result = timelineGenerator.generate(itemsNoLane, config, null)
|
|
expect(result).toContain('Ohne Epic')
|
|
})
|
|
|
|
it('should respect timelineMonths setting', () => {
|
|
const shortConfig = { ...config, settings: { timelineMonths: 6 } }
|
|
const result = timelineGenerator.generate(items, shortConfig, null)
|
|
|
|
// Should create 6 months worth of grid lines
|
|
const lineCount = (result.match(/<line/g) || []).length
|
|
expect(lineCount).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should escape special characters in item text', () => {
|
|
const itemsWithSpecialChars = [{
|
|
id: 'T&1',
|
|
title: 'Task with <special> & "characters"',
|
|
lane: 'Development',
|
|
due: new Date('2025-01-01')
|
|
}]
|
|
|
|
const result = timelineGenerator.generate(itemsWithSpecialChars, config, null)
|
|
|
|
expect(result).toContain('T&1')
|
|
expect(result).toContain('<special> & "characters"')
|
|
})
|
|
|
|
it('should determine start date from earliest item', () => {
|
|
const itemsWithEarlyDate = [
|
|
{ id: 'T-1', title: 'Early', lane: 'Dev', due: new Date('2024-06-15') },
|
|
{ id: 'T-2', title: 'Late', lane: 'Dev', due: new Date('2025-12-01') }
|
|
]
|
|
|
|
const result = timelineGenerator.generate(itemsWithEarlyDate, config, null)
|
|
|
|
// Should start from June 2024 (first day of month)
|
|
expect(result).toContain('Jun 24')
|
|
})
|
|
|
|
it('should clamp item positions to timeline bounds', () => {
|
|
const itemsOutOfRange = [
|
|
{ id: 'T-1', title: 'In Range', lane: 'Dev', due: new Date('2025-01-01') },
|
|
{ id: 'T-2', title: 'Way Future', lane: 'Dev', due: new Date('2030-01-01') }
|
|
]
|
|
|
|
// Should not throw and should generate valid SVG
|
|
const result = timelineGenerator.generate(itemsOutOfRange, config, null)
|
|
expect(result).toContain('<svg')
|
|
expect(result).toContain('T-1')
|
|
expect(result).toContain('T-2')
|
|
})
|
|
})
|
|
}) |