diff --git a/__tests__/utils/export.test.ts b/__tests__/utils/export.test.ts new file mode 100644 index 0000000..cec4d47 --- /dev/null +++ b/__tests__/utils/export.test.ts @@ -0,0 +1,227 @@ +/** + * Tests for the export utility functions + * @jest-environment jsdom + */ + +import { exportToCSV, exportToJSON, formatDateForExport } from '@/utils/export'; + +describe('Export Utilities', () => { + // Mock DOM methods for file download + let mockClick: jest.Mock; + let mockLink: Partial; + const originalCreateObjectURL = URL.createObjectURL; + const originalRevokeObjectURL = URL.revokeObjectURL; + + beforeAll(() => { + // Mock URL methods globally + URL.createObjectURL = jest.fn(() => 'blob:test-url'); + URL.revokeObjectURL = jest.fn(); + }); + + afterAll(() => { + URL.createObjectURL = originalCreateObjectURL; + URL.revokeObjectURL = originalRevokeObjectURL; + }); + + beforeEach(() => { + mockClick = jest.fn(); + mockLink = { + href: '', + download: '', + style: { display: '' } as CSSStyleDeclaration, + click: mockClick, + }; + + jest.spyOn(document, 'createElement').mockReturnValue(mockLink as HTMLAnchorElement); + jest.spyOn(document.body, 'appendChild').mockImplementation(() => mockLink as Node); + jest.spyOn(document.body, 'removeChild').mockImplementation(() => mockLink as Node); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('exportToCSV', () => { + it('should create and download a CSV file', () => { + const data = [ + ['Header1', 'Header2'], + ['Value1', 'Value2'], + ]; + + exportToCSV(data, 'test-file'); + + expect(document.createElement).toHaveBeenCalledWith('a'); + expect(URL.createObjectURL).toHaveBeenCalled(); + expect(document.body.appendChild).toHaveBeenCalled(); + expect(mockClick).toHaveBeenCalled(); + expect(document.body.removeChild).toHaveBeenCalled(); + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:test-url'); + }); + + it('should include date in filename by default', () => { + const data = [['test']]; + const today = new Date().toISOString().split('T')[0]; + + exportToCSV(data, 'test-file'); + + expect(mockLink.download).toContain('test-file'); + expect(mockLink.download).toContain(today); + expect(mockLink.download).toContain('.csv'); + }); + + it('should exclude date when includeDate is false', () => { + const data = [['test']]; + + exportToCSV(data, 'test-file', { includeDate: false }); + + expect(mockLink.download).toBe('test-file.csv'); + }); + + it('should use custom separator', () => { + const data = [['A', 'B'], ['C', 'D']]; + + // We can't easily verify the content of the blob, but we can verify the function runs + expect(() => exportToCSV(data, 'test', { separator: ';' })).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should handle null and undefined values', () => { + const data = [[null, undefined, 'value']]; + + expect(() => exportToCSV(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should escape values containing separator', () => { + const data = [['value,with,commas', 'normal']]; + + expect(() => exportToCSV(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should escape values containing quotes', () => { + const data = [['value"with"quotes', 'normal']]; + + expect(() => exportToCSV(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should escape values containing newlines', () => { + const data = [['value\nwith\nnewlines', 'normal']]; + + expect(() => exportToCSV(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should handle boolean values', () => { + const data = [[true, false]]; + + expect(() => exportToCSV(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should handle number values', () => { + const data = [[123, 456.78]]; + + expect(() => exportToCSV(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should set link style to hidden', () => { + const data = [['test']]; + + exportToCSV(data, 'test'); + + expect(mockLink.style?.display).toBe('none'); + }); + }); + + describe('exportToJSON', () => { + it('should create and download a JSON file', () => { + const data = { key: 'value', nested: { a: 1 } }; + + exportToJSON(data, 'test-file'); + + expect(document.createElement).toHaveBeenCalledWith('a'); + expect(URL.createObjectURL).toHaveBeenCalled(); + expect(document.body.appendChild).toHaveBeenCalled(); + expect(mockClick).toHaveBeenCalled(); + expect(document.body.removeChild).toHaveBeenCalled(); + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:test-url'); + }); + + it('should include date in filename', () => { + const data = { test: true }; + const today = new Date().toISOString().split('T')[0]; + + exportToJSON(data, 'test-file'); + + expect(mockLink.download).toContain('test-file'); + expect(mockLink.download).toContain(today); + expect(mockLink.download).toContain('.json'); + }); + + it('should handle arrays', () => { + const data = [1, 2, 3, { a: 'b' }]; + + expect(() => exportToJSON(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should handle nested objects', () => { + const data = { + level1: { + level2: { + level3: 'deep', + }, + }, + }; + + expect(() => exportToJSON(data, 'test')).not.toThrow(); + expect(mockClick).toHaveBeenCalled(); + }); + + it('should set link style to hidden', () => { + const data = { test: true }; + + exportToJSON(data, 'test'); + + expect(mockLink.style?.display).toBe('none'); + }); + }); + + describe('formatDateForExport', () => { + it('should format date string correctly', () => { + const result = formatDateForExport('2024-01-15T10:30:00Z'); + + // French format: DD/MM/YYYY HH:MM + expect(result).toMatch(/\d{2}\/\d{2}\/\d{4}/); + }); + + it('should format Date object correctly', () => { + const date = new Date('2024-01-15T10:30:00Z'); + const result = formatDateForExport(date); + + expect(result).toMatch(/\d{2}\/\d{2}\/\d{4}/); + }); + + it('should return empty string for null', () => { + expect(formatDateForExport(null)).toBe(''); + }); + + it('should return empty string for undefined', () => { + expect(formatDateForExport(undefined)).toBe(''); + }); + + it('should return empty string for empty string', () => { + expect(formatDateForExport('')).toBe(''); + }); + + it('should include time in the format', () => { + const result = formatDateForExport('2024-01-15T10:30:00Z'); + + // Should contain time component + expect(result).toMatch(/\d{2}:\d{2}/); + }); + }); +});