import { describe, it, expect } from 'vitest'; import { UseCaseOutputPort } from './UseCaseOutputPort'; describe('UseCaseOutputPort', () => { describe('UseCaseOutputPort interface', () => { it('should have present method', () => { const presentedData: unknown[] = []; const outputPort: UseCaseOutputPort = { present: (data: string) => { presentedData.push(data); } }; outputPort.present('test data'); expect(presentedData).toHaveLength(1); expect(presentedData[0]).toBe('test data'); }); it('should support different data types', () => { const presentedData: Array<{ type: string; data: unknown }> = []; const outputPort: UseCaseOutputPort = { present: (data: unknown) => { presentedData.push({ type: typeof data, data }); } }; outputPort.present('string data'); outputPort.present(42); outputPort.present({ id: 1, name: 'test' }); outputPort.present([1, 2, 3]); expect(presentedData).toHaveLength(4); expect(presentedData[0]).toEqual({ type: 'string', data: 'string data' }); expect(presentedData[1]).toEqual({ type: 'number', data: 42 }); expect(presentedData[2]).toEqual({ type: 'object', data: { id: 1, name: 'test' } }); expect(presentedData[3]).toEqual({ type: 'object', data: [1, 2, 3] }); }); it('should support complex data structures', () => { interface UserDTO { id: string; name: string; email: string; profile: { avatar: string; bio: string; preferences: { theme: 'light' | 'dark'; notifications: boolean; }; }; metadata: { createdAt: Date; updatedAt: Date; lastLogin?: Date; }; } const presentedUsers: UserDTO[] = []; const outputPort: UseCaseOutputPort = { present: (data: UserDTO) => { presentedUsers.push(data); } }; const user: UserDTO = { id: 'user-123', name: 'John Doe', email: 'john@example.com', profile: { avatar: 'avatar.jpg', bio: 'Software developer', preferences: { theme: 'dark', notifications: true } }, metadata: { createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-02'), lastLogin: new Date('2024-01-03') } }; outputPort.present(user); expect(presentedUsers).toHaveLength(1); expect(presentedUsers[0]).toEqual(user); }); it('should support array data', () => { const presentedArrays: number[][] = []; const outputPort: UseCaseOutputPort = { present: (data: number[]) => { presentedArrays.push(data); } }; outputPort.present([1, 2, 3]); outputPort.present([4, 5, 6]); expect(presentedArrays).toHaveLength(2); expect(presentedArrays[0]).toEqual([1, 2, 3]); expect(presentedArrays[1]).toEqual([4, 5, 6]); }); it('should support null and undefined values', () => { const presentedValues: unknown[] = []; const outputPort: UseCaseOutputPort = { present: (data: unknown) => { presentedValues.push(data); } }; outputPort.present(null); outputPort.present(undefined); outputPort.present('value'); expect(presentedValues).toHaveLength(3); expect(presentedValues[0]).toBe(null); expect(presentedValues[1]).toBe(undefined); expect(presentedValues[2]).toBe('value'); }); }); describe('UseCaseOutputPort behavior', () => { it('should support transformation before presentation', () => { const presentedData: Array<{ transformed: string; original: string }> = []; const outputPort: UseCaseOutputPort = { present: (data: string) => { const transformed = data.toUpperCase(); presentedData.push({ transformed, original: data }); } }; outputPort.present('hello'); outputPort.present('world'); expect(presentedData).toHaveLength(2); expect(presentedData[0]).toEqual({ transformed: 'HELLO', original: 'hello' }); expect(presentedData[1]).toEqual({ transformed: 'WORLD', original: 'world' }); }); it('should support validation before presentation', () => { const presentedData: string[] = []; const validationErrors: string[] = []; const outputPort: UseCaseOutputPort = { present: (data: string) => { if (data.length === 0) { validationErrors.push('Data cannot be empty'); return; } if (data.length > 100) { validationErrors.push('Data exceeds maximum length'); return; } presentedData.push(data); } }; outputPort.present('valid data'); outputPort.present(''); outputPort.present('a'.repeat(101)); outputPort.present('another valid'); expect(presentedData).toHaveLength(2); expect(presentedData[0]).toBe('valid data'); expect(presentedData[1]).toBe('another valid'); expect(validationErrors).toHaveLength(2); expect(validationErrors[0]).toBe('Data cannot be empty'); expect(validationErrors[1]).toBe('Data exceeds maximum length'); }); it('should support pagination', () => { interface PaginatedResult { items: T[]; total: number; page: number; totalPages: number; } const presentedPages: PaginatedResult[] = []; const outputPort: UseCaseOutputPort> = { present: (data: PaginatedResult) => { presentedPages.push(data); } }; const page1: PaginatedResult = { items: ['item-1', 'item-2', 'item-3'], total: 10, page: 1, totalPages: 4 }; const page2: PaginatedResult = { items: ['item-4', 'item-5', 'item-6'], total: 10, page: 2, totalPages: 4 }; outputPort.present(page1); outputPort.present(page2); expect(presentedPages).toHaveLength(2); expect(presentedPages[0]).toEqual(page1); expect(presentedPages[1]).toEqual(page2); }); it('should support streaming presentation', () => { const stream: string[] = []; const outputPort: UseCaseOutputPort = { present: (data: string) => { stream.push(data); } }; // Simulate streaming data const chunks = ['chunk-1', 'chunk-2', 'chunk-3', 'chunk-4']; chunks.forEach(chunk => outputPort.present(chunk)); expect(stream).toHaveLength(4); expect(stream).toEqual(chunks); }); it('should support error handling in presentation', () => { const presentedData: string[] = []; const presentationErrors: Error[] = []; const outputPort: UseCaseOutputPort = { present: (data: string) => { try { // Simulate complex presentation logic that might fail if (data === 'error') { throw new Error('Presentation failed'); } presentedData.push(data); } catch (error) { if (error instanceof Error) { presentationErrors.push(error); } } } }; outputPort.present('valid-1'); outputPort.present('error'); outputPort.present('valid-2'); expect(presentedData).toHaveLength(2); expect(presentedData[0]).toBe('valid-1'); expect(presentedData[1]).toBe('valid-2'); expect(presentationErrors).toHaveLength(1); expect(presentationErrors[0].message).toBe('Presentation failed'); }); }); describe('UseCaseOutputPort implementation patterns', () => { it('should support console presenter', () => { const consoleOutputs: string[] = []; const originalConsoleLog = console.log; // Mock console.log console.log = (...args: unknown[]) => consoleOutputs.push(args.join(' ')); const consolePresenter: UseCaseOutputPort = { present: (data: string) => { console.log('Presented:', data); } }; consolePresenter.present('test data'); // Restore console.log console.log = originalConsoleLog; expect(consoleOutputs).toHaveLength(1); expect(consoleOutputs[0]).toContain('Presented:'); expect(consoleOutputs[0]).toContain('test data'); }); it('should support HTTP response presenter', () => { const responses: Array<{ status: number; body: unknown; headers?: Record }> = []; const httpResponsePresenter: UseCaseOutputPort = { present: (data: unknown) => { responses.push({ status: 200, body: data, headers: { 'content-type': 'application/json' } }); } }; httpResponsePresenter.present({ id: 1, name: 'test' }); expect(responses).toHaveLength(1); expect(responses[0].status).toBe(200); expect(responses[0].body).toEqual({ id: 1, name: 'test' }); expect(responses[0].headers).toEqual({ 'content-type': 'application/json' }); }); it('should support WebSocket presenter', () => { const messages: Array<{ type: string; data: unknown; timestamp: string }> = []; const webSocketPresenter: UseCaseOutputPort = { present: (data: unknown) => { messages.push({ type: 'data', data, timestamp: new Date().toISOString() }); } }; webSocketPresenter.present({ event: 'user-joined', userId: 'user-123' }); expect(messages).toHaveLength(1); expect(messages[0].type).toBe('data'); expect(messages[0].data).toEqual({ event: 'user-joined', userId: 'user-123' }); expect(messages[0].timestamp).toBeDefined(); }); it('should support event bus presenter', () => { const events: Array<{ topic: string; payload: unknown; metadata: unknown }> = []; const eventBusPresenter: UseCaseOutputPort = { present: (data: unknown) => { events.push({ topic: 'user-events', payload: data, metadata: { source: 'use-case', timestamp: new Date().toISOString() } }); } }; eventBusPresenter.present({ userId: 'user-123', action: 'created' }); expect(events).toHaveLength(1); expect(events[0].topic).toBe('user-events'); expect(events[0].payload).toEqual({ userId: 'user-123', action: 'created' }); expect(events[0].metadata).toMatchObject({ source: 'use-case' }); }); it('should support batch presenter', () => { const batches: Array<{ items: unknown[]; batchSize: number; processedAt: string }> = []; let currentBatch: unknown[] = []; const batchSize = 3; const batchPresenter: UseCaseOutputPort = { present: (data: unknown) => { currentBatch.push(data); if (currentBatch.length >= batchSize) { batches.push({ items: [...currentBatch], batchSize: currentBatch.length, processedAt: new Date().toISOString() }); currentBatch = []; } } }; // Present 7 items for (let i = 1; i <= 7; i++) { batchPresenter.present({ item: i }); } // Add remaining items if (currentBatch.length > 0) { batches.push({ items: [...currentBatch], batchSize: currentBatch.length, processedAt: new Date().toISOString() }); } expect(batches).toHaveLength(3); expect(batches[0].items).toHaveLength(3); expect(batches[1].items).toHaveLength(3); expect(batches[2].items).toHaveLength(1); }); it('should support caching presenter', () => { const cache = new Map(); const cacheHits: string[] = []; const cacheMisses: string[] = []; const cachingPresenter: UseCaseOutputPort<{ key: string; data: unknown }> = { present: (data: { key: string; data: unknown }) => { if (cache.has(data.key)) { cacheHits.push(data.key); // Update cache with new data even if key exists cache.set(data.key, data.data); } else { cacheMisses.push(data.key); cache.set(data.key, data.data); } } }; cachingPresenter.present({ key: 'user-1', data: { name: 'John' } }); cachingPresenter.present({ key: 'user-2', data: { name: 'Jane' } }); cachingPresenter.present({ key: 'user-1', data: { name: 'John Updated' } }); expect(cacheHits).toHaveLength(1); expect(cacheHits[0]).toBe('user-1'); expect(cacheMisses).toHaveLength(2); expect(cacheMisses[0]).toBe('user-1'); expect(cacheMisses[1]).toBe('user-2'); expect(cache.get('user-1')).toEqual({ name: 'John Updated' }); }); }); });