import { describe, it, expect, beforeEach } from 'vitest' import { createSampleItems, createSampleProject, createSampleTemplate, createMalformedTemplate } 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 = '' 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('Template validation', () => { let items, config beforeEach(() => { items = createSampleItems() config = createSampleProject() }) it('should throw error when template is not provided', () => { expect(() => { timelineGenerator.generate(items, config, null) }).toThrow('Template is required') }) it('should throw error when template is missing month-template', () => { const malformedTemplate = createMalformedTemplate('month-template') expect(() => { timelineGenerator.generate(items, config, malformedTemplate) }).toThrow('Template is missing required elements: month-template') }) it('should throw error when template is missing lane-template', () => { const malformedTemplate = createMalformedTemplate('lane-template') expect(() => { timelineGenerator.generate(items, config, malformedTemplate) }).toThrow('Template is missing required elements: lane-template') }) it('should throw error when template is missing item-template', () => { const malformedTemplate = createMalformedTemplate('item-template') expect(() => { timelineGenerator.generate(items, config, malformedTemplate) }).toThrow('Template is missing required elements: item-template') }) it('should validate template with proper error message', () => { const emptyTemplate = '' expect(() => { timelineGenerator.generate(items, config, emptyTemplate) }).toThrow('Please use a template-v2.svg file with proper template elements') }) }) describe('Template extraction', () => { it('should extract template element by id', () => { const svg = ` {{LABEL}} ` const result = timelineGenerator.extractTemplate(svg, 'month-template') expect(result).toContain('id="month-template"') expect(result).toContain('{{X}}') expect(result).toContain('{{LABEL}}') }) it('should throw error when template not found', () => { const svg = '' expect(() => { timelineGenerator.extractTemplate(svg, 'month-template') }).toThrow('Failed to extract template element: month-template') }) it('should extract nested elements within template', () => { const svg = ` Label ` const result = timelineGenerator.extractTemplate(svg, 'lane-template') expect(result).toContain('') expect(result).toContain('Label') }) }) describe('Placeholder replacement', () => { it('should replace all placeholders with values', () => { const template = '{{LABEL}}' const values = { X: 100, Y: 200, LABEL: 'Test' } const result = timelineGenerator.replacePlaceholders(template, values) expect(result).toBe('Test') }) it('should handle multiple occurrences of same placeholder', () => { const template = '' const values = { X: 50 } const result = timelineGenerator.replacePlaceholders(template, values) expect(result).toBe('') }) it('should leave unmatched placeholders unchanged', () => { const template = '{{LABEL}} {{OTHER}}' const values = { LABEL: 'Test' } const result = timelineGenerator.replacePlaceholders(template, values) expect(result).toContain('Test') expect(result).toContain('{{OTHER}}') }) it('should handle numeric and boolean values', () => { const template = '' const values = { X: 42, VISIBLE: true } const result = timelineGenerator.replacePlaceholders(template, values) expect(result).toBe('') }) }) describe('Template-based SVG generation', () => { let items, config beforeEach(() => { items = createSampleItems() config = createSampleProject() }) it('should generate SVG with template-v2 format', () => { const template = createSampleTemplate() const result = timelineGenerator.generate(items, config, template) expect(result).toContain('') expect(result).not.toContain('{{MONTHS}}') expect(result).not.toContain('{{LANES}}') }) it('should not contain template placeholders in output', () => { const template = createSampleTemplate() const result = timelineGenerator.generate(items, config, template) expect(result).not.toContain('{{MONTH_X}}') expect(result).not.toContain('{{LANE_Y}}') expect(result).not.toContain('{{ITEM_X}}') expect(result).not.toContain('{{ITEM_TITLE}}') }) it('should contain task data in generated SVG', () => { const template = createSampleTemplate() const result = timelineGenerator.generate(items, config, template) expect(result).toContain('T-1') expect(result).toContain('First Task') expect(result).toContain('T-2') expect(result).toContain('Second Task') expect(result).toContain('T-3') expect(result).toContain('Third Task') }) it('should contain lane names in generated SVG', () => { const template = createSampleTemplate() const result = timelineGenerator.generate(items, config, template) expect(result).toContain('Development') expect(result).toContain('Testing') }) it('should escape special characters in task data', () => { const itemsWithSpecialChars = [{ id: 'T&1', title: 'Task with & "characters"', lane: 'Development', due: new Date('2025-01-01') }] const template = createSampleTemplate() const result = timelineGenerator.generate(itemsWithSpecialChars, config, template) expect(result).toContain('T&1') expect(result).toContain('<special> & "characters"') }) it('should handle items without lanes', () => { const itemsNoLane = [ { id: 'T-1', title: 'No Lane Task', lane: null, due: new Date('2025-01-01') } ] const template = createSampleTemplate() const result = timelineGenerator.generate(itemsNoLane, config, template) expect(result).toContain('Ohne Epic') }) it('should respect timelineMonths setting', () => { const template = createSampleTemplate() const shortConfig = { ...config, settings: { timelineMonths: 6 } } const longConfig = { ...config, settings: { timelineMonths: 24 } } const shortResult = timelineGenerator.generate(items, shortConfig, template) const longResult = timelineGenerator.generate(items, longConfig, template) // Longer timeline should produce larger SVG const shortWidth = shortResult.match(/width="(\d+)"/)?.[1] || 0 const longWidth = longResult.match(/width="(\d+)"/)?.[1] || 0 expect(parseInt(longWidth)).toBeGreaterThan(parseInt(shortWidth)) }) it('should not contain template elements with display:none in output', () => { const template = createSampleTemplate() const result = timelineGenerator.generate(items, config, template) expect(result).not.toContain('id="month-template"') expect(result).not.toContain('id="lane-template"') expect(result).not.toContain('id="item-template"') expect(result).not.toContain('style="display:none"') }) it('should preserve template styling in generated output', async () => { const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8') const result = timelineGenerator.generate(items, config, templateV2) // Should preserve gradient and filter definitions expect(result).toContain('monthHeaderGrad') expect(result).toContain('textShadow') expect(result).toContain('bgGrid') }) it('should set viewBox dimensions correctly', () => { const template = createSampleTemplate() const result = timelineGenerator.generate(items, config, template) const widthMatch = result.match(/width="(\d+)"/) const heightMatch = result.match(/height="(\d+)"/) const viewBoxMatch = result.match(/viewBox="0 0 (\d+) (\d+)"/) expect(widthMatch).toBeTruthy() expect(heightMatch).toBeTruthy() expect(viewBoxMatch).toBeTruthy() // viewBox should match width and height expect(viewBoxMatch[1]).toBe(widthMatch[1]) expect(viewBoxMatch[2]).toBe(heightMatch[1]) }) it('should support custom placeholder mapping', () => { const customConfig = { ...config, placeholderMapping: { id: 'TASK_ID', // Map item.id to {{TASK_ID}} instead of {{ITEM_ID}} title: 'TASK_NAME' // Map item.title to {{TASK_NAME}} instead of {{ITEM_TITLE}} } } // Create template with custom placeholders const customTemplate = ` {{MONTHS}} {{LANES}} ` const result = timelineGenerator.generate(items, customConfig, customTemplate) // Should use custom placeholder names expect(result).toContain('T-1: First Task') // TASK_ID: TASK_NAME expect(result).toContain('T-2: Second Task') // Should not contain default placeholder syntax expect(result).not.toContain('{{ITEM_ID}}') expect(result).not.toContain('{{ITEM_TITLE}}') }) it('should use default convention when placeholderMapping is not provided', () => { const template = createSampleTemplate() const result = timelineGenerator.generate(items, config, template) // Should use default ITEM_* convention expect(result).toContain('T-1 First Task') // Default template has space, not colon expect(result).not.toContain('{{ITEM_ID}}') expect(result).not.toContain('{{ITEM_TITLE}}') }) }) })