import { describe, it, expect, beforeEach, vi, type Mock } from 'bun:test'; import Logger from '../src/Logger'; import { LoggingLevel } from '../src/level/LoggingLevel'; import Threshold from '../src/level/Threshold'; import type IAppender from '../src/appenders/IAppender'; describe('Logger', () => { let mockAppender: IAppender; let threshold: Threshold; let logger: Logger; beforeEach(() => { mockAppender = { log: vi.fn(), }; threshold = new Threshold(LoggingLevel.Info); logger = new Logger('test-category', threshold, new Set([mockAppender])); }); describe('threshold filtering', () => { it('should log info messages when threshold is Info', () => { logger.info('test message'); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), // timestamp 'Info', 'test-category', 'test message' ); }); it('should log warn messages when threshold is Info', () => { logger.warn('test message'); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Warn', 'test-category', 'test message' ); }); it('should log error messages when threshold is Info', () => { logger.error('test message'); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Error', 'test-category', 'test message' ); }); it('should not log debug messages when threshold is Info', () => { logger.debug('test message'); expect(mockAppender.log).not.toHaveBeenCalled(); }); it('should not log trace messages when threshold is Info', () => { logger.trace('test message'); expect(mockAppender.log).not.toHaveBeenCalled(); }); it('should not log silly messages when threshold is Info', () => { logger.silly('test message'); expect(mockAppender.log).not.toHaveBeenCalled(); }); it('should log all messages when threshold is Debug', () => { threshold.value = LoggingLevel.Debug; logger.debug('debug message'); logger.info('info message'); logger.warn('warn message'); logger.error('error message'); expect(mockAppender.log).toHaveBeenCalledTimes(4); }); it('should log only error messages when threshold is Error', () => { threshold.value = LoggingLevel.Error; logger.error('error message'); logger.warn('warn message'); logger.info('info message'); expect(mockAppender.log).toHaveBeenCalledTimes(1); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Error', 'test-category', 'error message' ); }); }); describe('message formatting', () => { beforeEach(() => { threshold.value = LoggingLevel.Debug; // Allow all messages for formatting tests }); it('should format messages with %s placeholder', () => { logger.info('Hello %s', 'world'); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'Hello world' ); }); it('should format messages with %d placeholder', () => { logger.info('Count: %d', 42); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'Count: 42' ); }); it('should format messages with %j placeholder', () => { logger.info('Object: %j', { key: 'value' }); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'Object: {"key":"value"}' ); }); it('should handle circular references in %j placeholder', () => { const obj: any = { key: 'value' }; obj.self = obj; logger.info('Circular: %j', obj); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'Circular: [Circular]' ); }); it('should handle multiple placeholders', () => { logger.info('User %s has %d items: %j', 'Alice', 5, ['a', 'b']); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'User Alice has 5 items: ["a","b"]' ); }); it('should handle more placeholders than arguments', () => { logger.info('Hello %s %s', 'world'); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'Hello world %s' ); }); it('should handle fewer placeholders than arguments', () => { logger.info('Hello %s', 'world', 'extra'); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'Hello world' ); }); it('should handle non-number values for %d', () => { logger.info('Value: %d', 'not-a-number'); expect(mockAppender.log).toHaveBeenCalledWith( expect.any(String), 'Info', 'test-category', 'Value: NaN' ); }); }); describe('timestamp formatting', () => { it('should include ISO timestamp in log messages', () => { const before = new Date().toISOString(); logger.info('test'); const after = new Date().toISOString(); const call = (mockAppender.log as Mock).mock.calls[0]; const timestamp = call[0]; expect(timestamp).toBeDefined(); expect(() => new Date(timestamp)).not.toThrow(); expect(timestamp >= before || timestamp <= after).toBe(true); }); }); describe('multiple appenders', () => { it('should call all appenders', () => { const mockAppender2: IAppender = { log: vi.fn(), }; const loggerWithMultiple = new Logger('test', threshold, new Set([mockAppender, mockAppender2])); loggerWithMultiple.info('test message'); expect(mockAppender.log).toHaveBeenCalledTimes(1); expect(mockAppender2.log).toHaveBeenCalledTimes(1); }); }); });