diff --git a/src/BreakpointContext.tsx b/src/BreakpointContext.tsx index feb6a47..7ddbefa 100644 --- a/src/BreakpointContext.tsx +++ b/src/BreakpointContext.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext, useMemo, useEffect } from 'react'; import { useResizeDetector } from 'react-resize-detector'; +import { logger } from './utils/logger'; export type Breakpoint = string; // Allow arbitrary breakpoint names @@ -96,9 +97,10 @@ export const BreakpointProvider: React.FC = ({ if (duplicates.length > 0) { if (shouldLog) { - console.error( - '❌ BreakpointProvider: Duplicate breakpoint values detected. This may lead to unexpected behavior.' - ); + logger.error('Duplicate breakpoint values detected', { + duplicates: duplicates.map(([key, value]) => ({ key, value })), + message: 'This may lead to unexpected behavior', + }); } } }, [sortedBreakpoints, shouldLog]); @@ -119,7 +121,10 @@ export const BreakpointProvider: React.FC = ({ useEffect(() => { if (width === undefined || width === 0) { if (shouldLog) { - console.error('❌ BreakpointProvider: element width is undefined or 0'); + logger.error('Element width is undefined or 0', { + width, + message: 'Element cannot be measured properly', + }); } } }, [width, shouldLog]); @@ -129,15 +134,20 @@ export const BreakpointProvider: React.FC = ({ if (width !== undefined && width > 0 && currentBreakpoint === null) { if (sortedBreakpoints.length > 0 && width < sortedBreakpoints[0][1]) { if (shouldLog) { - console.error( - `❌ BreakpointProvider: The current width (${width}px) is less than the smallest breakpoint value (${sortedBreakpoints[0][1]}px). Consider including a breakpoint with a value of 0 to cover all cases.` - ); + logger.error('Current width is less than smallest breakpoint', { + currentWidth: width, + smallestBreakpoint: sortedBreakpoints[0][1], + smallestBreakpointName: sortedBreakpoints[0][0], + suggestion: 'Consider including a breakpoint with a value of 0 to cover all cases', + }); } } else { if (shouldLog) { - console.error( - '❌ BreakpointProvider: No breakpoint could be determined from the provided configuration. Check your breakpoints object.' - ); + logger.error('No breakpoint could be determined', { + width, + breakpointsConfig: sortedBreakpoints, + suggestion: 'Check your breakpoints object configuration', + }); } } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..dd5c7b0 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,61 @@ +export interface Logger { + debug(message: string, data?: Record): void; + info(message: string, data?: Record): void; + warn(message: string, data?: Record): void; + error(message: string, data?: Record): void; +} + +interface LogEntry { + level: 'debug' | 'info' | 'warn' | 'error'; + message: string; + data?: Record; + timestamp: string; + context: string; +} + +class BreakpointLogger implements Logger { + private shouldLog(level: string): boolean { + const isProduction = process.env.NODE_ENV === 'production'; + if (isProduction) return level === 'error'; + return true; // Log everything in non-production environments + } + + private formatMessage(level: string, message: string, data?: Record): void { + if (!this.shouldLog(level)) return; + + const entry: LogEntry = { + level: level as LogEntry['level'], + message, + data, + timestamp: new Date().toISOString(), + context: 'BreakpointProvider', + }; + + // Use appropriate console method based on level + const consoleMethod = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log; + + const isProduction = process.env.NODE_ENV === 'production'; + // In production, only log errors. In non-production, log everything. + if (!isProduction || level === 'error') { + consoleMethod(`[${entry.context}] ${entry.message}`, entry.data || ''); + } + } + + debug(message: string, data?: Record): void { + this.formatMessage('debug', message, data); + } + + info(message: string, data?: Record): void { + this.formatMessage('info', message, data); + } + + warn(message: string, data?: Record): void { + this.formatMessage('warn', message, data); + } + + error(message: string, data?: Record): void { + this.formatMessage('error', message, data); + } +} + +export const logger = new BreakpointLogger(); diff --git a/tests/BreakpointContext.test.tsx b/tests/BreakpointContext.test.tsx index b0b7948..e4468a5 100644 --- a/tests/BreakpointContext.test.tsx +++ b/tests/BreakpointContext.test.tsx @@ -111,7 +111,12 @@ describe('BreakpointContext', () => { ); expect(console.error).toHaveBeenCalledWith( - expect.stringContaining('The current width (100px) is less than the smallest breakpoint value (200px).') + '[BreakpointProvider] Current width is less than smallest breakpoint', + expect.objectContaining({ + currentWidth: 100, + smallestBreakpoint: 200, + smallestBreakpointName: 'XS' + }) ); }); @@ -128,7 +133,11 @@ describe('BreakpointContext', () => { ); expect(console.error).toHaveBeenCalledWith( - '❌ BreakpointProvider: Duplicate breakpoint values detected. This may lead to unexpected behavior.' + '[BreakpointProvider] Duplicate breakpoint values detected', + expect.objectContaining({ + duplicates: expect.any(Array), + message: 'This may lead to unexpected behavior' + }) ); }); }); \ No newline at end of file diff --git a/tests/logger.test.ts b/tests/logger.test.ts new file mode 100644 index 0000000..cc6b8f2 --- /dev/null +++ b/tests/logger.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { logger } from '../src/utils/logger'; + +describe('Logger', () => { + let consoleSpy: { + log: ReturnType; + warn: ReturnType; + error: ReturnType; + }; + + beforeEach(() => { + consoleSpy = { + log: vi.spyOn(console, 'log').mockImplementation(() => {}), + warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), + error: vi.spyOn(console, 'error').mockImplementation(() => {}) + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + // Reset NODE_ENV to original value + process.env.NODE_ENV = 'test'; + }); + + describe('in development mode', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development'; + }); + + it('should log debug messages', () => { + logger.debug('Test debug message', { key: 'value' }); + + expect(consoleSpy.log).toHaveBeenCalledWith( + '[BreakpointProvider] Test debug message', + { key: 'value' } + ); + }); + + it('should log info messages', () => { + logger.info('Test info message', { key: 'value' }); + + expect(consoleSpy.log).toHaveBeenCalledWith( + '[BreakpointProvider] Test info message', + { key: 'value' } + ); + }); + + it('should log warn messages', () => { + logger.warn('Test warn message', { key: 'value' }); + + expect(consoleSpy.warn).toHaveBeenCalledWith( + '[BreakpointProvider] Test warn message', + { key: 'value' } + ); + }); + + it('should log error messages', () => { + logger.error('Test error message', { key: 'value' }); + + expect(consoleSpy.error).toHaveBeenCalledWith( + '[BreakpointProvider] Test error message', + { key: 'value' } + ); + }); + + it('should handle logging without data parameter', () => { + logger.info('Test message without data'); + + expect(consoleSpy.log).toHaveBeenCalledWith( + '[BreakpointProvider] Test message without data', + '' + ); + }); + }); + + describe('in production mode', () => { + beforeEach(() => { + process.env.NODE_ENV = 'production'; + }); + + it('should not log debug messages', () => { + logger.debug('Test debug message'); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + }); + + it('should not log info messages', () => { + logger.info('Test info message'); + + expect(consoleSpy.log).not.toHaveBeenCalled(); + }); + + it('should not log warn messages', () => { + logger.warn('Test warn message'); + + expect(consoleSpy.warn).not.toHaveBeenCalled(); + }); + + it('should still log error messages', () => { + logger.error('Test error message', { key: 'value' }); + + expect(consoleSpy.error).toHaveBeenCalledWith( + '[BreakpointProvider] Test error message', + { key: 'value' } + ); + }); + }); + + describe('in test mode (non-development, non-production)', () => { + beforeEach(() => { + process.env.NODE_ENV = 'test'; + }); + + it('should log all message types in test mode', () => { + logger.debug('Debug in test'); + logger.info('Info in test'); + logger.warn('Warn in test'); + logger.error('Error in test'); + + expect(consoleSpy.log).toHaveBeenCalledTimes(2); // debug and info + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); + expect(consoleSpy.error).toHaveBeenCalledTimes(1); + }); + }); + + describe('structured data logging', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development'; + }); + + it('should log complex structured data correctly', () => { + const complexData = { + duplicates: [ + { key: 'SM', value: 500 }, + { key: 'MD', value: 500 } + ], + message: 'This may lead to unexpected behavior' + }; + + logger.error('Duplicate breakpoint values detected', complexData); + + expect(consoleSpy.error).toHaveBeenCalledWith( + '[BreakpointProvider] Duplicate breakpoint values detected', + complexData + ); + }); + + it('should handle nested object data', () => { + const nestedData = { + currentWidth: 400, + smallestBreakpoint: 500, + smallestBreakpointName: 'SM', + suggestion: 'Consider including a breakpoint with a value of 0' + }; + + logger.error('Current width is less than smallest breakpoint', nestedData); + + expect(consoleSpy.error).toHaveBeenCalledWith( + '[BreakpointProvider] Current width is less than smallest breakpoint', + nestedData + ); + }); + }); +}); \ No newline at end of file