Skip to content
Open
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
31 changes: 9 additions & 22 deletions src/__tests__/components/GeometryCanvas.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ describe('GeometryCanvas', () => {
// This was causing incomplete grid lines when the dev tools were closed.
});

test('arrow keys should adjust navigation step size correctly', () => {
test('should render and handle keyboard events without errors', () => {
// Mock a formula for testing
const mockFormula: Formula = {
id: 'test-formula',
Expand Down Expand Up @@ -155,27 +155,14 @@ describe('GeometryCanvas', () => {
expect(canvas).not.toBeNull();

if (canvas) {
// Set up a spy on console.log to capture the step size changes
const consoleSpy = jest.spyOn(console, 'log');

// We need to directly test the implementation of the step size adjustment
// Since we can't easily simulate selecting a formula point in the test

// Simulate pressing the up arrow key to increase step size
fireEvent.keyDown(canvas, { key: 'ArrowUp' });

// Simulate pressing the down arrow key to decrease step size
fireEvent.keyDown(canvas, { key: 'ArrowDown' });

// Create a test that verifies the step size increment was changed from 0.1 to 0.01
// This is a more direct test of the fix we made

// Check that the console logs show the key events were captured
expect(consoleSpy).toHaveBeenCalledWith('Key down:', 'ArrowUp');
expect(consoleSpy).toHaveBeenCalledWith('Key down:', 'ArrowDown');

// Clean up
consoleSpy.mockRestore();
// Test that keyboard events don't cause errors in the refactored component
expect(() => {
fireEvent.keyDown(canvas, { key: 'ArrowUp' });
fireEvent.keyDown(canvas, { key: 'ArrowDown' });
fireEvent.keyDown(canvas, { key: 'Shift' });
fireEvent.keyUp(canvas, { key: 'Shift' });
fireEvent.keyDown(canvas, { key: 'Delete' });
}).not.toThrow();
}
});
});
118 changes: 118 additions & 0 deletions src/__tests__/utils/calibrationHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
getStoredCalibrationValue,
storeCalibrationValue,
getDefaultCalibrationValue,
getAllCalibrationData,
clearAllCalibrationData,
} from '@/utils/calibrationHelper';
import { MeasurementUnit } from '@/types/shapes';

// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};

Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});

describe('calibrationHelper', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('getDefaultCalibrationValue', () => {
it('should return correct defaults for each unit', () => {
expect(getDefaultCalibrationValue('cm')).toBe(60);
expect(getDefaultCalibrationValue('in')).toBe(152.4);
});

it('should return cm default for unknown units', () => {
expect(getDefaultCalibrationValue('unknown' as MeasurementUnit)).toBe(60);
});
});

describe('getStoredCalibrationValue', () => {
it('should return stored value when available', () => {
localStorageMock.getItem.mockReturnValue('80');

const result = getStoredCalibrationValue('cm');

expect(localStorageMock.getItem).toHaveBeenCalledWith('geometry-canvas-cm');
expect(result).toBe(80);
});

it('should return default value when no stored value', () => {
localStorageMock.getItem.mockReturnValue(null);

const result = getStoredCalibrationValue('cm');

expect(result).toBe(60); // default for cm
});

it('should return default value for invalid stored value', () => {
localStorageMock.getItem.mockReturnValue('invalid');

const result = getStoredCalibrationValue('cm');

expect(result).toBe(60); // default for cm
});
});

describe('storeCalibrationValue', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should store valid calibration value', () => {
storeCalibrationValue('cm', 80);

expect(localStorageMock.setItem).toHaveBeenCalledWith('geometry-canvas-cm', '80');
});

it('should not store invalid calibration value', () => {
storeCalibrationValue('cm', -10);

// Should not call setItem for the calibration value, but isLocalStorageAvailable check still calls it
expect(localStorageMock.setItem).toHaveBeenCalledWith('test', 'test');
expect(localStorageMock.setItem).not.toHaveBeenCalledWith('geometry-canvas-cm', '-10');
});

it('should not store zero calibration value', () => {
storeCalibrationValue('cm', 0);

// Should not call setItem for the calibration value, but isLocalStorageAvailable check still calls it
expect(localStorageMock.setItem).toHaveBeenCalledWith('test', 'test');
expect(localStorageMock.setItem).not.toHaveBeenCalledWith('geometry-canvas-cm', '0');
});
});

describe('getAllCalibrationData', () => {
it('should return calibration data for all units', () => {
localStorageMock.getItem.mockImplementation((key) => {
if (key === 'geometry-canvas-cm') return '80';
if (key === 'geometry-canvas-in') return '160';
return null;
});

const result = getAllCalibrationData();

expect(result).toEqual({
pixelsPerCm: 80,
pixelsPerInch: 160,
});
});
});

describe('clearAllCalibrationData', () => {
it('should clear all stored calibration data', () => {
clearAllCalibrationData();

expect(localStorageMock.removeItem).toHaveBeenCalledWith('geometry-canvas-cm');
expect(localStorageMock.removeItem).toHaveBeenCalledWith('geometry-canvas-in');
});
});
});
66 changes: 66 additions & 0 deletions src/__tests__/utils/canvasUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createDebouncedOriginUpdate } from '@/utils/canvasUtils';

describe('canvasUtils', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

describe('createDebouncedOriginUpdate', () => {
it('should debounce function calls', () => {
const mockCallback = jest.fn();
const debouncedUpdate = createDebouncedOriginUpdate(mockCallback);

// Call multiple times quickly
debouncedUpdate({ x: 1, y: 1 });
debouncedUpdate({ x: 2, y: 2 });
debouncedUpdate({ x: 3, y: 3 });

// Should not have been called yet
expect(mockCallback).not.toHaveBeenCalled();

// Fast-forward time
jest.advanceTimersByTime(50);

// Should be called with the last value only
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({ x: 3, y: 3 });
});

it('should cancel previous timeouts when called multiple times', () => {
const mockCallback = jest.fn();
const debouncedUpdate = createDebouncedOriginUpdate(mockCallback);

// Call and advance time partially
debouncedUpdate({ x: 1, y: 1 });
jest.advanceTimersByTime(25); // Half the debounce time

// Call again - should reset the timer
debouncedUpdate({ x: 2, y: 2 });
jest.advanceTimersByTime(25); // Should not trigger yet

expect(mockCallback).not.toHaveBeenCalled();

// Complete the debounce time
jest.advanceTimersByTime(25);

expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({ x: 2, y: 2 });
});

it('should call callback after debounce time has passed', () => {
const mockCallback = jest.fn();
const debouncedUpdate = createDebouncedOriginUpdate(mockCallback);

debouncedUpdate({ x: 10, y: 20 });

jest.advanceTimersByTime(50);

expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({ x: 10, y: 20 });
});
});
});
53 changes: 53 additions & 0 deletions src/__tests__/utils/logging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { logger, isVerboseLoggingEnabled } from '@/utils/logging';

// Mock console methods
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {
// Default implementation - will be overridden by provider
});
const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {
// Default implementation - will be overridden by provider
});
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {
// Default implementation - will be overridden by provider
});

describe('logging utility', () => {
beforeEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
mockConsoleLog.mockRestore();
mockConsoleWarn.mockRestore();
mockConsoleError.mockRestore();
});

describe('logger', () => {
it('should log debug messages when verbose logging is enabled', () => {
if (isVerboseLoggingEnabled()) {
logger.debug('test debug message');
expect(mockConsoleLog).toHaveBeenCalledWith('[DEBUG]', 'test debug message');
} else {
logger.debug('test debug message');
expect(mockConsoleLog).not.toHaveBeenCalled();
}
});

it('should always log error messages', () => {
logger.error('test error message');
expect(mockConsoleError).toHaveBeenCalledWith('[ERROR]', 'test error message');
});

it('should log warning messages when enabled', () => {
logger.warn('test warning message');
// Warnings should be logged in most environments
expect(mockConsoleWarn).toHaveBeenCalledWith('[WARN]', 'test warning message');
});
});

describe('isVerboseLoggingEnabled', () => {
it('should return a boolean', () => {
expect(typeof isVerboseLoggingEnabled()).toBe('boolean');
});
});
});
6 changes: 5 additions & 1 deletion src/components/FormulaGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface FormulaGraphProps {
pointIndex?: number;
allPoints?: FormulaPoint[];
navigationStepSize?: number;
gridPosition?: { x: number; y: number };
pixelsPerUnit?: number;
} | null) => void;
globalSelectedPoint?: (FormulaPoint & {
mathX: number;
Expand Down Expand Up @@ -505,7 +507,9 @@ const FormulaGraph: React.FC<FormulaGraphProps> = ({
mathY,
formula,
pointIndex: closestPointIndex,
allPoints: points
allPoints: points,
gridPosition,
pixelsPerUnit
});
} else {
// If no point is close enough, clear the selection
Expand Down
76 changes: 76 additions & 0 deletions src/components/GeometryCanvas/FormulaLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import FormulaGraph from '../FormulaGraph';
import { Formula, FormulaPoint } from '@/types/formula';
import { Point } from '@/types/shapes';
import { Z_INDEX } from '@/utils/constants';

interface FormulaLayerProps {
formulas?: Formula[];
gridPosition: Point | null;
zoomedPixelsPerUnit: number;
selectedPoint: {
x: number;
y: number;
mathX: number;
mathY: number;
formula: Formula;
pointIndex?: number;
allPoints?: FormulaPoint[];
navigationStepSize?: number;
isValid: boolean;
} | null;
onPointSelect: (point: {
x: number;
y: number;
mathX: number;
mathY: number;
formula: Formula;
pointIndex?: number;
allPoints?: FormulaPoint[];
navigationStepSize?: number;
gridPosition?: { x: number; y: number };
pixelsPerUnit?: number;
isValid: boolean;
} | null) => void;
}

/**
* Renders all formula-related layers
*/
const FormulaLayer: React.FC<FormulaLayerProps> = React.memo(({
formulas,
gridPosition,
zoomedPixelsPerUnit,
selectedPoint,
onPointSelect,
}) => {
// Early return if no formulas or grid position
if (!formulas || formulas.length === 0 || !gridPosition) {
return null;
}

return (
<div className="absolute inset-0" style={{ zIndex: Z_INDEX.FORMULAS, pointerEvents: 'none' }}>
<svg
width="100%"
height="100%"
style={{ pointerEvents: 'none' }}
>
{formulas.map(formula => (
<FormulaGraph
key={formula.id}
formula={formula}
gridPosition={gridPosition}
pixelsPerUnit={zoomedPixelsPerUnit}
onPointSelect={onPointSelect}
globalSelectedPoint={selectedPoint}
/>
))}
</svg>
</div>
);
});

FormulaLayer.displayName = 'FormulaLayer';

export default FormulaLayer;
Loading
Loading