import { describe, it, expect, beforeEach, vi } from 'vitest' import { setupBasicDOM, createMockElement } from './setup.js' import { createSampleProject, createSampleCSV, mockFetch, mockPapaParse, expectElementToHaveText } from './testHelpers.js' // Import the engine by loading it as text and evaluating // This is needed because engine.js creates a global object const fs = await import('fs/promises') const engineCode = await fs.readFile('./engine.js', 'utf-8') describe('Timeline Engine', () => { let timelineEngine beforeEach(async () => { setupBasicDOM() // Reset global window object global.window = global global.timelineGenerator = { generate: vi.fn() } // Execute engine code to create timelineEngine eval(engineCode) timelineEngine = global.window.timelineEngine }) describe('parseDate', () => { it('should parse YYYY-MM-DD format', () => { const result = timelineEngine.parseDate('2025-12-15') expect(result).toEqual(new Date(2025, 11, 15)) }) it('should parse YYYY/MM/DD format', () => { const result = timelineEngine.parseDate('2025/12/15') expect(result).toEqual(new Date(2025, 11, 15)) }) it('should parse DD.MM.YYYY format', () => { const result = timelineEngine.parseDate('15.12.2025') expect(result).toEqual(new Date(2025, 11, 15)) }) it('should return null for invalid dates', () => { expect(timelineEngine.parseDate('invalid')).toBeNull() expect(timelineEngine.parseDate('')).toBeNull() expect(timelineEngine.parseDate(null)).toBeNull() }) it('should handle single digit months and days', () => { const result = timelineEngine.parseDate('2025-1-5') expect(result).toEqual(new Date(2025, 0, 5)) }) }) describe('loadProjectConfigObject', () => { it('should load project configuration and update DOM', async () => { const config = createSampleProject() mockFetch('') mockFetch(createSampleCSV()) mockPapaParse() await timelineEngine.loadProjectConfigObject(config) expect(timelineEngine.config).toEqual(config) expectElementToHaveText('#projectName', 'Test Project') expectElementToHaveText('#projectSubtitle', 'A test project for unit testing') }) it('should handle missing stylesheet gracefully', async () => { const config = { name: 'Test', description: 'Test desc' } await timelineEngine.loadProjectConfigObject(config) expect(timelineEngine.config).toEqual(config) }) it('should not override CSS when cssOverride is true', async () => { timelineEngine.cssOverride = true const config = createSampleProject() // Still need to mock SVG template and CSV fetches mockFetch('') mockFetch(createSampleCSV()) mockPapaParse() await timelineEngine.loadProjectConfigObject(config) // Should not load the stylesheet from config const href = document.getElementById('dynamicCss').href expect(href).not.toContain(config.stylesheet) }) it('should not load CSV when csvOverride is true', async () => { timelineEngine.csvOverride = true const config = createSampleProject() const processCsvSpy = vi.spyOn(timelineEngine, 'processCsv') // Still need to mock SVG template fetch mockFetch('') await timelineEngine.loadProjectConfigObject(config) expect(processCsvSpy).not.toHaveBeenCalled() }) }) describe('processCsv', () => { beforeEach(() => { timelineEngine.config = createSampleProject() global.Papa.parse.mockImplementation((text, options) => { const mockData = [ { ID: 'T-1', Title: 'Task 1', Lane: 'Dev', Due: '2025-01-15' }, { ID: 'T-2', Title: 'Task 2', Lane: 'Test', Due: '2025-02-20' }, { ID: '', Title: '', Lane: '', Due: '' } // Empty row should be filtered ] options.complete({ data: mockData }) }) }) it('should process CSV and generate timeline', () => { global.timelineGenerator.generate.mockReturnValue('test') timelineEngine.processCsv(createSampleCSV()) expect(global.Papa.parse).toHaveBeenCalled() expect(global.timelineGenerator.generate).toHaveBeenCalled() expect(document.getElementById('viewer').innerHTML).toBe('test') }) it('should filter out items without title or due date', () => { global.timelineGenerator.generate.mockImplementation((items) => { expect(items).toHaveLength(2) // Empty row should be filtered out return 'test' }) timelineEngine.processCsv(createSampleCSV()) }) it('should handle missing config gracefully', () => { timelineEngine.config = null const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) timelineEngine.processCsv(createSampleCSV()) expect(consoleSpy).toHaveBeenCalledWith('No config or fieldMapping found.') consoleSpy.mockRestore() }) it('should show message when no valid items found', () => { global.Papa.parse.mockImplementation((text, options) => { options.complete({ data: [] }) }) timelineEngine.processCsv('') expect(document.getElementById('viewer').innerHTML).toContain('Keine gültigen Items gefunden') }) it('should enable download button after successful generation', () => { global.timelineGenerator.generate.mockReturnValue('test') const downloadBtn = document.getElementById('downloadSvg') timelineEngine.processCsv(createSampleCSV()) expect(downloadBtn.disabled).toBe(false) expect(downloadBtn.style.opacity).toBe('1') }) }) describe('autoLoadDefaultProject', () => { it('should try binect/project.json first, then my-project, then example', async () => { // First call (binect) fails global.fetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' }) // Second call (my-project) fails global.fetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' }) // Third call (example) succeeds mockFetch(createSampleProject()) mockFetch('') mockFetch(createSampleCSV()) await timelineEngine.autoLoadDefaultProject() expect(global.fetch).toHaveBeenCalledWith('binect/project.json') expect(global.fetch).toHaveBeenCalledWith('my-project/project.json') expect(global.fetch).toHaveBeenCalledWith('example/project.json') }) it('should handle all fetch failures gracefully', async () => { global.fetch.mockRejectedValue(new Error('Network error')) // Should not throw await expect(timelineEngine.autoLoadDefaultProject()).resolves.toBeUndefined() }) }) })