Skip to content

Commit 84f267b

Browse files
Copilotmfittko
andcommitted
Create utilities and hooks for refactoring
Co-authored-by: mfittko <326798+mfittko@users.noreply.github.com>
1 parent 3304cca commit 84f267b

13 files changed

Lines changed: 934 additions & 0 deletions
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
getStoredCalibrationValue,
3+
storeCalibrationValue,
4+
getDefaultCalibrationValue,
5+
getAllCalibrationData,
6+
clearAllCalibrationData,
7+
} from '@/utils/calibrationHelper';
8+
import { MeasurementUnit } from '@/types/shapes';
9+
10+
// Mock localStorage
11+
const localStorageMock = {
12+
getItem: jest.fn(),
13+
setItem: jest.fn(),
14+
removeItem: jest.fn(),
15+
clear: jest.fn(),
16+
};
17+
18+
Object.defineProperty(window, 'localStorage', {
19+
value: localStorageMock,
20+
});
21+
22+
describe('calibrationHelper', () => {
23+
beforeEach(() => {
24+
jest.clearAllMocks();
25+
});
26+
27+
describe('getDefaultCalibrationValue', () => {
28+
it('should return correct defaults for each unit', () => {
29+
expect(getDefaultCalibrationValue('cm')).toBe(60);
30+
expect(getDefaultCalibrationValue('in')).toBe(152.4);
31+
});
32+
33+
it('should return cm default for unknown units', () => {
34+
expect(getDefaultCalibrationValue('unknown' as MeasurementUnit)).toBe(60);
35+
});
36+
});
37+
38+
describe('getStoredCalibrationValue', () => {
39+
it('should return stored value when available', () => {
40+
localStorageMock.getItem.mockReturnValue('80');
41+
42+
const result = getStoredCalibrationValue('cm');
43+
44+
expect(localStorageMock.getItem).toHaveBeenCalledWith('geometry-canvas-cm');
45+
expect(result).toBe(80);
46+
});
47+
48+
it('should return default value when no stored value', () => {
49+
localStorageMock.getItem.mockReturnValue(null);
50+
51+
const result = getStoredCalibrationValue('cm');
52+
53+
expect(result).toBe(60); // default for cm
54+
});
55+
56+
it('should return default value for invalid stored value', () => {
57+
localStorageMock.getItem.mockReturnValue('invalid');
58+
59+
const result = getStoredCalibrationValue('cm');
60+
61+
expect(result).toBe(60); // default for cm
62+
});
63+
});
64+
65+
describe('storeCalibrationValue', () => {
66+
beforeEach(() => {
67+
jest.clearAllMocks();
68+
});
69+
70+
it('should store valid calibration value', () => {
71+
storeCalibrationValue('cm', 80);
72+
73+
expect(localStorageMock.setItem).toHaveBeenCalledWith('geometry-canvas-cm', '80');
74+
});
75+
76+
it('should not store invalid calibration value', () => {
77+
storeCalibrationValue('cm', -10);
78+
79+
// Should not call setItem for the calibration value, but isLocalStorageAvailable check still calls it
80+
expect(localStorageMock.setItem).toHaveBeenCalledWith('test', 'test');
81+
expect(localStorageMock.setItem).not.toHaveBeenCalledWith('geometry-canvas-cm', '-10');
82+
});
83+
84+
it('should not store zero calibration value', () => {
85+
storeCalibrationValue('cm', 0);
86+
87+
// Should not call setItem for the calibration value, but isLocalStorageAvailable check still calls it
88+
expect(localStorageMock.setItem).toHaveBeenCalledWith('test', 'test');
89+
expect(localStorageMock.setItem).not.toHaveBeenCalledWith('geometry-canvas-cm', '0');
90+
});
91+
});
92+
93+
describe('getAllCalibrationData', () => {
94+
it('should return calibration data for all units', () => {
95+
localStorageMock.getItem.mockImplementation((key) => {
96+
if (key === 'geometry-canvas-cm') return '80';
97+
if (key === 'geometry-canvas-in') return '160';
98+
return null;
99+
});
100+
101+
const result = getAllCalibrationData();
102+
103+
expect(result).toEqual({
104+
pixelsPerCm: 80,
105+
pixelsPerInch: 160,
106+
});
107+
});
108+
});
109+
110+
describe('clearAllCalibrationData', () => {
111+
it('should clear all stored calibration data', () => {
112+
clearAllCalibrationData();
113+
114+
expect(localStorageMock.removeItem).toHaveBeenCalledWith('geometry-canvas-cm');
115+
expect(localStorageMock.removeItem).toHaveBeenCalledWith('geometry-canvas-in');
116+
});
117+
});
118+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { createDebouncedOriginUpdate } from '@/utils/canvasUtils';
2+
3+
describe('canvasUtils', () => {
4+
beforeEach(() => {
5+
jest.useFakeTimers();
6+
});
7+
8+
afterEach(() => {
9+
jest.useRealTimers();
10+
});
11+
12+
describe('createDebouncedOriginUpdate', () => {
13+
it('should debounce function calls', () => {
14+
const mockCallback = jest.fn();
15+
const debouncedUpdate = createDebouncedOriginUpdate(mockCallback);
16+
17+
// Call multiple times quickly
18+
debouncedUpdate({ x: 1, y: 1 });
19+
debouncedUpdate({ x: 2, y: 2 });
20+
debouncedUpdate({ x: 3, y: 3 });
21+
22+
// Should not have been called yet
23+
expect(mockCallback).not.toHaveBeenCalled();
24+
25+
// Fast-forward time
26+
jest.advanceTimersByTime(50);
27+
28+
// Should be called with the last value only
29+
expect(mockCallback).toHaveBeenCalledTimes(1);
30+
expect(mockCallback).toHaveBeenCalledWith({ x: 3, y: 3 });
31+
});
32+
33+
it('should cancel previous timeouts when called multiple times', () => {
34+
const mockCallback = jest.fn();
35+
const debouncedUpdate = createDebouncedOriginUpdate(mockCallback);
36+
37+
// Call and advance time partially
38+
debouncedUpdate({ x: 1, y: 1 });
39+
jest.advanceTimersByTime(25); // Half the debounce time
40+
41+
// Call again - should reset the timer
42+
debouncedUpdate({ x: 2, y: 2 });
43+
jest.advanceTimersByTime(25); // Should not trigger yet
44+
45+
expect(mockCallback).not.toHaveBeenCalled();
46+
47+
// Complete the debounce time
48+
jest.advanceTimersByTime(25);
49+
50+
expect(mockCallback).toHaveBeenCalledTimes(1);
51+
expect(mockCallback).toHaveBeenCalledWith({ x: 2, y: 2 });
52+
});
53+
54+
it('should call callback after debounce time has passed', () => {
55+
const mockCallback = jest.fn();
56+
const debouncedUpdate = createDebouncedOriginUpdate(mockCallback);
57+
58+
debouncedUpdate({ x: 10, y: 20 });
59+
60+
jest.advanceTimersByTime(50);
61+
62+
expect(mockCallback).toHaveBeenCalledTimes(1);
63+
expect(mockCallback).toHaveBeenCalledWith({ x: 10, y: 20 });
64+
});
65+
});
66+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { logger, isVerboseLoggingEnabled } from '@/utils/logging';
2+
3+
// Mock console methods
4+
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
5+
const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
6+
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
7+
8+
describe('logging utility', () => {
9+
beforeEach(() => {
10+
jest.clearAllMocks();
11+
});
12+
13+
afterAll(() => {
14+
mockConsoleLog.mockRestore();
15+
mockConsoleWarn.mockRestore();
16+
mockConsoleError.mockRestore();
17+
});
18+
19+
describe('logger', () => {
20+
it('should log debug messages when verbose logging is enabled', () => {
21+
if (isVerboseLoggingEnabled()) {
22+
logger.debug('test debug message');
23+
expect(mockConsoleLog).toHaveBeenCalledWith('[DEBUG]', 'test debug message');
24+
} else {
25+
logger.debug('test debug message');
26+
expect(mockConsoleLog).not.toHaveBeenCalled();
27+
}
28+
});
29+
30+
it('should always log error messages', () => {
31+
logger.error('test error message');
32+
expect(mockConsoleError).toHaveBeenCalledWith('[ERROR]', 'test error message');
33+
});
34+
35+
it('should log warning messages when enabled', () => {
36+
logger.warn('test warning message');
37+
// Warnings should be logged in most environments
38+
expect(mockConsoleWarn).toHaveBeenCalledWith('[WARN]', 'test warning message');
39+
});
40+
});
41+
42+
describe('isVerboseLoggingEnabled', () => {
43+
it('should return a boolean', () => {
44+
expect(typeof isVerboseLoggingEnabled()).toBe('boolean');
45+
});
46+
});
47+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
import FormulaGraph from '../FormulaGraph';
3+
import { Formula, FormulaPoint } from '@/types/formula';
4+
import { Point } from '@/types/shapes';
5+
import { Z_INDEX } from '@/utils/constants';
6+
7+
interface FormulaLayerProps {
8+
formulas?: Formula[];
9+
gridPosition: Point | null;
10+
zoomedPixelsPerUnit: number;
11+
selectedPoint: {
12+
x: number;
13+
y: number;
14+
mathX: number;
15+
mathY: number;
16+
formula: Formula;
17+
pointIndex?: number;
18+
allPoints?: FormulaPoint[];
19+
navigationStepSize?: number;
20+
isValid: boolean;
21+
} | null;
22+
onPointSelect: (point: {
23+
x: number;
24+
y: number;
25+
mathX: number;
26+
mathY: number;
27+
formula: Formula;
28+
pointIndex?: number;
29+
allPoints?: FormulaPoint[];
30+
navigationStepSize?: number;
31+
isValid: boolean;
32+
} | null) => void;
33+
}
34+
35+
/**
36+
* Renders all formula-related layers
37+
*/
38+
const FormulaLayer: React.FC<FormulaLayerProps> = React.memo(({
39+
formulas,
40+
gridPosition,
41+
zoomedPixelsPerUnit,
42+
selectedPoint,
43+
onPointSelect,
44+
}) => {
45+
// Early return if no formulas or grid position
46+
if (!formulas || formulas.length === 0 || !gridPosition) {
47+
return null;
48+
}
49+
50+
return (
51+
<div style={{ zIndex: Z_INDEX.FORMULAS }}>
52+
{formulas.map(formula => (
53+
<FormulaGraph
54+
key={formula.id}
55+
formula={formula}
56+
gridPosition={gridPosition}
57+
pixelsPerUnit={zoomedPixelsPerUnit}
58+
onPointSelect={onPointSelect}
59+
globalSelectedPoint={selectedPoint}
60+
/>
61+
))}
62+
</div>
63+
);
64+
});
65+
66+
FormulaLayer.displayName = 'FormulaLayer';
67+
68+
export default FormulaLayer;

0 commit comments

Comments
 (0)