Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/BreakpointContext.tsx
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -96,9 +97,10 @@ export const BreakpointProvider: React.FC<BreakpointProviderProps> = ({

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]);
Expand All @@ -119,7 +121,10 @@ export const BreakpointProvider: React.FC<BreakpointProviderProps> = ({
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]);
Expand All @@ -129,15 +134,20 @@ export const BreakpointProvider: React.FC<BreakpointProviderProps> = ({
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',
});
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export interface Logger {
debug(message: string, data?: Record<string, unknown>): void;
info(message: string, data?: Record<string, unknown>): void;
warn(message: string, data?: Record<string, unknown>): void;
error(message: string, data?: Record<string, unknown>): void;
}

interface LogEntry {
level: 'debug' | 'info' | 'warn' | 'error';
message: string;
data?: Record<string, unknown>;
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<string, unknown>): 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<string, unknown>): void {
this.formatMessage('debug', message, data);
}

info(message: string, data?: Record<string, unknown>): void {
this.formatMessage('info', message, data);
}

warn(message: string, data?: Record<string, unknown>): void {
this.formatMessage('warn', message, data);
}

error(message: string, data?: Record<string, unknown>): void {
this.formatMessage('error', message, data);
}
}

export const logger = new BreakpointLogger();
13 changes: 11 additions & 2 deletions tests/BreakpointContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,12 @@ describe('BreakpointContext', () => {
</BreakpointProvider>
);
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'
})
);
});

Expand All @@ -128,7 +133,11 @@ describe('BreakpointContext', () => {
</BreakpointProvider>
);
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'
})
);
});
});
164 changes: 164 additions & 0 deletions tests/logger.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof vi.spyOn>;
warn: ReturnType<typeof vi.spyOn>;
error: ReturnType<typeof vi.spyOn>;
};

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
);
});
});
});