feature/fetch-fonts #14

Merged
ilia merged 76 commits from feature/fetch-fonts into main 2026-01-14 11:01:44 +00:00
3 changed files with 634 additions and 0 deletions
Showing only changes of commit 36a326817d - Show all commits

View File

@@ -0,0 +1,176 @@
/**
* Tests for clampNumber utility
*/
import {
describe,
expect,
test,
} from 'vitest';
import { clampNumber } from './clampNumber';
describe('clampNumber', () => {
describe('basic functionality', () => {
test('should return value when within range', () => {
expect(clampNumber(5, 0, 10)).toBe(5);
expect(clampNumber(0.5, 0, 1)).toBe(0.5);
expect(clampNumber(-3, -10, 10)).toBe(-3);
});
test('should clamp value to minimum', () => {
expect(clampNumber(-5, 0, 10)).toBe(0);
expect(clampNumber(-100, -50, 100)).toBe(-50);
expect(clampNumber(0, 1, 10)).toBe(1);
});
test('should clamp value to maximum', () => {
expect(clampNumber(15, 0, 10)).toBe(10);
expect(clampNumber(150, -50, 100)).toBe(100);
expect(clampNumber(100, 1, 50)).toBe(50);
});
test('should handle boundary values', () => {
expect(clampNumber(0, 0, 10)).toBe(0);
expect(clampNumber(10, 0, 10)).toBe(10);
expect(clampNumber(-5, -5, 5)).toBe(-5);
expect(clampNumber(5, -5, 5)).toBe(5);
});
});
describe('negative ranges', () => {
test('should handle fully negative ranges', () => {
expect(clampNumber(-5, -10, -1)).toBe(-5);
expect(clampNumber(-15, -10, -1)).toBe(-10);
expect(clampNumber(-0.5, -10, -1)).toBe(-1);
});
test('should handle ranges spanning zero', () => {
expect(clampNumber(0, -10, 10)).toBe(0);
expect(clampNumber(-5, -10, 10)).toBe(-5);
expect(clampNumber(5, -10, 10)).toBe(5);
});
});
describe('floating-point numbers', () => {
test('should clamp floating-point values correctly', () => {
expect(clampNumber(0.75, 0, 1)).toBe(0.75);
expect(clampNumber(1.5, 0, 1)).toBe(1);
expect(clampNumber(-0.25, 0, 1)).toBe(0);
});
test('should handle very small decimals', () => {
expect(clampNumber(0.001, 0, 0.01)).toBe(0.001);
expect(clampNumber(0.1, 0, 0.01)).toBe(0.01);
});
test('should handle large floating-point numbers', () => {
expect(clampNumber(123.456, 100, 200)).toBe(123.456);
expect(clampNumber(99.999, 100, 200)).toBe(100);
expect(clampNumber(200.001, 100, 200)).toBe(200);
});
});
describe('edge cases', () => {
test('should handle when min equals max', () => {
expect(clampNumber(5, 10, 10)).toBe(10);
expect(clampNumber(10, 10, 10)).toBe(10);
expect(clampNumber(15, 10, 10)).toBe(10);
expect(clampNumber(0, 0, 0)).toBe(0);
});
test('should handle zero values', () => {
expect(clampNumber(0, 0, 10)).toBe(0);
expect(clampNumber(0, -10, 10)).toBe(0);
expect(clampNumber(5, 0, 0)).toBe(0);
});
test('should handle reversed min/max (min > max)', () => {
// When min > max, Math.max/Math.min will still produce a result
// but it's logically incorrect - we test the actual behavior
// Math.min(Math.max(5, 10), 0) = Math.min(10, 0) = 0
expect(clampNumber(5, 10, 0)).toBe(0);
expect(clampNumber(15, 10, 0)).toBe(0);
expect(clampNumber(-5, 10, 0)).toBe(0);
});
});
describe('special number values', () => {
test('should handle Infinity', () => {
expect(clampNumber(Infinity, 0, 10)).toBe(10);
expect(clampNumber(-Infinity, 0, 10)).toBe(0);
expect(clampNumber(5, -Infinity, Infinity)).toBe(5);
});
test('should handle NaN', () => {
expect(clampNumber(NaN, 0, 10)).toBeNaN();
});
});
describe('real-world scenarios', () => {
test('should clamp font size values', () => {
// Typical font size range: 8px to 72px
expect(clampNumber(16, 8, 72)).toBe(16);
expect(clampNumber(4, 8, 72)).toBe(8);
expect(clampNumber(100, 8, 72)).toBe(72);
});
test('should clamp slider values', () => {
// Slider range: 0 to 100
expect(clampNumber(50, 0, 100)).toBe(50);
expect(clampNumber(-10, 0, 100)).toBe(0);
expect(clampNumber(150, 0, 100)).toBe(100);
});
test('should clamp opacity values', () => {
// Opacity range: 0 to 1
expect(clampNumber(0.5, 0, 1)).toBe(0.5);
expect(clampNumber(-0.2, 0, 1)).toBe(0);
expect(clampNumber(1.2, 0, 1)).toBe(1);
});
test('should clamp percentage values', () => {
// Percentage range: 0 to 100
expect(clampNumber(75, 0, 100)).toBe(75);
expect(clampNumber(-5, 0, 100)).toBe(0);
expect(clampNumber(105, 0, 100)).toBe(100);
});
test('should clamp coordinate values', () => {
// Canvas coordinates: 0 to 800 width, 0 to 600 height
expect(clampNumber(400, 0, 800)).toBe(400);
expect(clampNumber(-50, 0, 800)).toBe(0);
expect(clampNumber(900, 0, 800)).toBe(800);
});
test('should clamp font weight values', () => {
// Font weight range: 100 to 900 (in increments of 100)
expect(clampNumber(400, 100, 900)).toBe(400);
expect(clampNumber(50, 100, 900)).toBe(100);
expect(clampNumber(950, 100, 900)).toBe(900);
});
test('should clamp line height values', () => {
// Line height range: 0.5 to 3.0
expect(clampNumber(1.5, 0.5, 3.0)).toBe(1.5);
expect(clampNumber(0.3, 0.5, 3.0)).toBe(0.5);
expect(clampNumber(4.0, 0.5, 3.0)).toBe(3.0);
});
});
describe('numeric constraints', () => {
test('should handle very large numbers', () => {
expect(clampNumber(Number.MAX_VALUE, 0, 100)).toBe(100);
expect(clampNumber(Number.MIN_VALUE, -10, 10)).toBe(Number.MIN_VALUE);
});
test('should handle negative infinity boundaries', () => {
expect(clampNumber(5, -Infinity, 10)).toBe(5);
expect(clampNumber(-1000, -Infinity, 10)).toBe(-1000);
});
test('should handle positive infinity boundaries', () => {
expect(clampNumber(5, 0, Infinity)).toBe(5);
expect(clampNumber(1000, 0, Infinity)).toBe(1000);
});
});
});

View File

@@ -0,0 +1,188 @@
/**
* Tests for getDecimalPlaces utility
*/
import {
describe,
expect,
test,
} from 'vitest';
import { getDecimalPlaces } from './getDecimalPlaces';
describe('getDecimalPlaces', () => {
describe('basic functionality', () => {
test('should return 0 for integers', () => {
expect(getDecimalPlaces(0)).toBe(0);
expect(getDecimalPlaces(1)).toBe(0);
expect(getDecimalPlaces(42)).toBe(0);
expect(getDecimalPlaces(-7)).toBe(0);
expect(getDecimalPlaces(1000)).toBe(0);
});
test('should return correct decimal places for decimals', () => {
expect(getDecimalPlaces(0.1)).toBe(1);
expect(getDecimalPlaces(0.5)).toBe(1);
expect(getDecimalPlaces(0.01)).toBe(2);
expect(getDecimalPlaces(0.05)).toBe(2);
expect(getDecimalPlaces(0.001)).toBe(3);
expect(getDecimalPlaces(0.123)).toBe(3);
expect(getDecimalPlaces(0.123456)).toBe(6);
});
test('should handle negative decimal numbers', () => {
expect(getDecimalPlaces(-0.1)).toBe(1);
expect(getDecimalPlaces(-0.05)).toBe(2);
expect(getDecimalPlaces(-1.5)).toBe(1);
expect(getDecimalPlaces(-99.99)).toBe(2);
});
});
describe('whole numbers with decimal part', () => {
test('should handle numbers with integer and decimal parts', () => {
expect(getDecimalPlaces(1.5)).toBe(1);
expect(getDecimalPlaces(10.25)).toBe(2);
expect(getDecimalPlaces(100.125)).toBe(3);
expect(getDecimalPlaces(1234.5678)).toBe(4);
});
test('should handle trailing zeros correctly', () => {
// Note: JavaScript string representation drops trailing zeros
expect(getDecimalPlaces(1.5)).toBe(1);
expect(getDecimalPlaces(1.50)).toBe(1); // 1.50 becomes "1.5" in string
});
});
describe('edge cases', () => {
test('should handle zero', () => {
expect(getDecimalPlaces(0)).toBe(0);
expect(getDecimalPlaces(0.0)).toBe(0);
});
test('should handle very small decimals', () => {
expect(getDecimalPlaces(0.0001)).toBe(4);
expect(getDecimalPlaces(0.00001)).toBe(5);
expect(getDecimalPlaces(0.000001)).toBe(6);
});
test('should handle very large numbers', () => {
expect(getDecimalPlaces(123456789.123)).toBe(3);
expect(getDecimalPlaces(999999.9999)).toBe(4);
});
test('should handle negative whole numbers', () => {
expect(getDecimalPlaces(-1)).toBe(0);
expect(getDecimalPlaces(-100)).toBe(0);
expect(getDecimalPlaces(-9999)).toBe(0);
});
});
describe('special number values', () => {
test('should handle Infinity', () => {
expect(getDecimalPlaces(Infinity)).toBe(0);
expect(getDecimalPlaces(-Infinity)).toBe(0);
});
test('should handle NaN', () => {
expect(getDecimalPlaces(NaN)).toBe(0);
});
});
describe('scientific notation', () => {
test('should handle numbers in scientific notation', () => {
// Very small numbers may be represented in scientific notation
const tiny = 1e-10;
const result = getDecimalPlaces(tiny);
// The result depends on how JS represents this as a string
expect(typeof result).toBe('number');
});
test('should handle large scientific notation numbers', () => {
const large = 1.23e5; // 123000
expect(getDecimalPlaces(large)).toBe(0);
});
});
describe('real-world scenarios', () => {
test('should handle currency values (2 decimal places)', () => {
expect(getDecimalPlaces(0.01)).toBe(2); // 1 cent
expect(getDecimalPlaces(0.99)).toBe(2); // 99 cents
// Note: JavaScript string representation drops trailing zeros
// 10.50 becomes "10.5" in string, so returns 1 decimal place
expect(getDecimalPlaces(10.50)).toBe(1); // $10.50
expect(getDecimalPlaces(999.99)).toBe(2); // $999.99
});
test('should handle measurement values', () => {
expect(getDecimalPlaces(12.5)).toBe(1); // 12.5 mm
expect(getDecimalPlaces(12.34)).toBe(2); // 12.34 cm
expect(getDecimalPlaces(12.345)).toBe(3); // 12.345 m
});
test('should handle step values for sliders', () => {
expect(getDecimalPlaces(0.1)).toBe(1); // Fine adjustment
expect(getDecimalPlaces(0.25)).toBe(2); // Quarter steps
expect(getDecimalPlaces(0.5)).toBe(1); // Half steps
expect(getDecimalPlaces(1)).toBe(0); // Whole steps
});
test('should handle font size increments', () => {
expect(getDecimalPlaces(0.5)).toBe(1); // Half point increments
expect(getDecimalPlaces(1)).toBe(0); // Whole point increments
});
test('should handle opacity values', () => {
expect(getDecimalPlaces(0.1)).toBe(1); // 10% increments
expect(getDecimalPlaces(0.05)).toBe(2); // 5% increments
expect(getDecimalPlaces(0.01)).toBe(2); // 1% increments
});
test('should handle percentage values', () => {
expect(getDecimalPlaces(0.5)).toBe(1); // 0.5%
expect(getDecimalPlaces(12.5)).toBe(1); // 12.5%
expect(getDecimalPlaces(33.33)).toBe(2); // 33.33%
});
test('should handle coordinate precision', () => {
expect(getDecimalPlaces(12.3456789)).toBe(7); // High precision GPS
expect(getDecimalPlaces(100.5)).toBe(1); // Low precision coordinates
});
test('should handle time values', () => {
expect(getDecimalPlaces(0.1)).toBe(1); // 100ms
expect(getDecimalPlaces(0.01)).toBe(2); // 10ms
expect(getDecimalPlaces(0.001)).toBe(3); // 1ms
});
});
describe('common step values', () => {
test('should correctly identify precision of common step values', () => {
expect(getDecimalPlaces(0.05)).toBe(2); // Very fine steps
expect(getDecimalPlaces(0.1)).toBe(1); // Fine steps
expect(getDecimalPlaces(0.25)).toBe(2); // Quarter steps
expect(getDecimalPlaces(0.5)).toBe(1); // Half steps
expect(getDecimalPlaces(1)).toBe(0); // Whole steps
expect(getDecimalPlaces(2)).toBe(0); // Even steps
expect(getDecimalPlaces(5)).toBe(0); // Five steps
expect(getDecimalPlaces(10)).toBe(0); // Ten steps
expect(getDecimalPlaces(25)).toBe(0); // Twenty-five steps
expect(getDecimalPlaces(50)).toBe(0); // Fifty steps
expect(getDecimalPlaces(100)).toBe(0); // Hundred steps
});
});
describe('floating-point representation', () => {
test('should handle standard floating-point representation', () => {
expect(getDecimalPlaces(1.1)).toBe(1);
expect(getDecimalPlaces(1.2)).toBe(1);
expect(getDecimalPlaces(1.3)).toBe(1);
});
test('should handle numbers that might have floating-point issues', () => {
// 0.1 + 0.2 = 0.30000000000000004 in JS
const sum = 0.1 + 0.2;
const places = getDecimalPlaces(sum);
// The function analyzes the string representation
expect(typeof places).toBe('number');
});
});
});

View File

@@ -0,0 +1,270 @@
/**
* Tests for roundToStepPrecision utility
*/
import {
describe,
expect,
test,
} from 'vitest';
import { roundToStepPrecision } from './roundToStepPrecision';
describe('roundToStepPrecision', () => {
describe('basic functionality', () => {
test('should return value unchanged for step=1', () => {
// step=1 has 0 decimal places, so it rounds to integers
expect(roundToStepPrecision(5, 1)).toBe(5);
expect(roundToStepPrecision(5.5, 1)).toBe(6); // rounds to nearest integer
expect(roundToStepPrecision(5.999, 1)).toBe(6);
});
test('should round to 1 decimal place for step=0.1', () => {
expect(roundToStepPrecision(1.23, 0.1)).toBeCloseTo(1.2);
expect(roundToStepPrecision(1.25, 0.1)).toBeCloseTo(1.3);
expect(roundToStepPrecision(1.29, 0.1)).toBeCloseTo(1.3);
});
test('should round to 2 decimal places for step=0.01', () => {
expect(roundToStepPrecision(1.234, 0.01)).toBeCloseTo(1.23);
expect(roundToStepPrecision(1.235, 0.01)).toBeCloseTo(1.24);
expect(roundToStepPrecision(1.239, 0.01)).toBeCloseTo(1.24);
});
test('should round to 3 decimal places for step=0.001', () => {
expect(roundToStepPrecision(1.2345, 0.001)).toBeCloseTo(1.235);
expect(roundToStepPrecision(1.2344, 0.001)).toBeCloseTo(1.234);
});
});
describe('floating-point precision issues', () => {
test('should fix floating-point precision errors with step=0.05', () => {
// Known floating-point issue: 0.1 + 0.05 = 0.15000000000000002
const value = 0.1 + 0.05;
const result = roundToStepPrecision(value, 0.05);
expect(result).toBeCloseTo(0.15, 2);
});
test('should fix floating-point errors with repeated additions', () => {
// Simulate adding 0.05 multiple times
let value = 1;
for (let i = 0; i < 10; i++) {
value += 0.05;
}
// value should be 1.5 but might be 1.4999999999999998
const result = roundToStepPrecision(value, 0.05);
expect(result).toBeCloseTo(1.5, 2);
});
test('should fix floating-point errors with step=0.1', () => {
// Known floating-point issue: 0.1 + 0.2 = 0.30000000000000004
const value = 0.1 + 0.2;
const result = roundToStepPrecision(value, 0.1);
expect(result).toBeCloseTo(0.3, 1);
});
test('should fix floating-point errors with step=0.01', () => {
// Known floating-point issue: 0.01 + 0.02 = 0.029999999999999999
const value = 0.01 + 0.02;
const result = roundToStepPrecision(value, 0.01);
expect(result).toBeCloseTo(0.03, 2);
});
test('should fix floating-point errors with step=0.25', () => {
const value = 0.5 + 0.25;
const result = roundToStepPrecision(value, 0.25);
expect(result).toBeCloseTo(0.75, 2);
});
test('should handle classic 0.1 + 0.2 problem', () => {
// Classic JavaScript floating-point issue
const value = 0.1 + 0.2;
// Without rounding: 0.30000000000000004
const result = roundToStepPrecision(value, 0.1);
expect(result).toBe(0.3);
});
});
describe('edge cases', () => {
test('should return value unchanged when step <= 0', () => {
expect(roundToStepPrecision(5, 0)).toBe(5);
expect(roundToStepPrecision(5, -1)).toBe(5);
expect(roundToStepPrecision(5, -0.5)).toBe(5);
});
test('should handle zero value', () => {
expect(roundToStepPrecision(0, 0.1)).toBe(0);
expect(roundToStepPrecision(0, 0.01)).toBe(0);
});
test('should handle negative values', () => {
expect(roundToStepPrecision(-1.234, 0.01)).toBeCloseTo(-1.23);
expect(roundToStepPrecision(-0.15, 0.05)).toBeCloseTo(-0.15);
expect(roundToStepPrecision(-5.5, 0.5)).toBeCloseTo(-5.5);
});
test('should handle very small step values', () => {
expect(roundToStepPrecision(1.1234, 0.0001)).toBeCloseTo(1.1234);
expect(roundToStepPrecision(1.12345, 0.0001)).toBeCloseTo(1.1235);
});
test('should handle very large values', () => {
expect(roundToStepPrecision(12345.6789, 0.01)).toBeCloseTo(12345.68);
expect(roundToStepPrecision(99999.9999, 0.001)).toBeCloseTo(100000);
});
});
describe('special number values', () => {
test('should handle Infinity', () => {
expect(roundToStepPrecision(Infinity, 0.1)).toBe(Infinity);
expect(roundToStepPrecision(-Infinity, 0.1)).toBe(-Infinity);
});
test('should handle NaN', () => {
expect(roundToStepPrecision(NaN, 0.1)).toBeNaN();
});
test('should handle step=Infinity', () => {
// getDecimalPlaces(Infinity) returns 0, so this rounds to 0 decimal places (integer)
const result = roundToStepPrecision(1.234, Infinity);
expect(result).toBeCloseTo(1);
});
});
describe('real-world scenarios', () => {
test('should handle currency calculations with step=0.01', () => {
// Add items with tax that might have floating-point errors
const subtotal = 10.99 + 5.99 + 2.99;
const rounded = roundToStepPrecision(subtotal, 0.01);
expect(rounded).toBeCloseTo(19.97, 2);
});
test('should handle slider values with step=0.1', () => {
// Slider value after multiple increments
let sliderValue = 0;
for (let i = 0; i < 15; i++) {
sliderValue += 0.1;
}
const rounded = roundToStepPrecision(sliderValue, 0.1);
expect(rounded).toBeCloseTo(1.5, 1);
});
test('should handle font size adjustments with step=0.5', () => {
// Font size adjustments
let fontSize = 12;
fontSize += 0.5; // 12.5
fontSize += 0.5; // 13.0
const rounded = roundToStepPrecision(fontSize, 0.5);
expect(rounded).toBeCloseTo(13, 1);
});
test('should handle opacity values with step=0.05', () => {
// Opacity from 0 to 1 in 5% increments
let opacity = 0;
for (let i = 0; i < 10; i++) {
opacity += 0.05;
}
const rounded = roundToStepPrecision(opacity, 0.05);
expect(rounded).toBeCloseTo(0.5, 2);
});
test('should handle percentage calculations with step=0.01', () => {
// Calculate percentage with floating-point issues
const percentage = (1 / 3) * 100;
const rounded = roundToStepPrecision(percentage, 0.01);
expect(rounded).toBeCloseTo(33.33, 2);
});
test('should handle coordinate rounding with step=0.000001', () => {
// GPS coordinates with micro-degree precision
const lat = 40.7128 + 0.000001;
const rounded = roundToStepPrecision(lat, 0.000001);
expect(rounded).toBeCloseTo(40.712801, 6);
});
test('should handle time values with step=0.001', () => {
// Millisecond precision timing
const time = 123.456 + 0.001 + 0.001;
const rounded = roundToStepPrecision(time, 0.001);
expect(rounded).toBeCloseTo(123.458, 3);
});
});
describe('common step values', () => {
test('should correctly round for step=0.05', () => {
// step=0.05 has 2 decimal places, so it rounds to 2 decimal places
// Note: This rounds to the DECIMAL PRECISION, not to the step increment
expect(roundToStepPrecision(1.34, 0.05)).toBeCloseTo(1.34);
expect(roundToStepPrecision(1.36, 0.05)).toBeCloseTo(1.36);
expect(roundToStepPrecision(1.37, 0.05)).toBeCloseTo(1.37);
expect(roundToStepPrecision(1.38, 0.05)).toBeCloseTo(1.38);
});
test('should correctly round for step=0.25', () => {
// step=0.25 has 2 decimal places, so it rounds to 2 decimal places
// Note: This rounds to the DECIMAL PRECISION, not to the step increment
expect(roundToStepPrecision(1.24, 0.25)).toBeCloseTo(1.24);
expect(roundToStepPrecision(1.26, 0.25)).toBeCloseTo(1.26);
expect(roundToStepPrecision(1.37, 0.25)).toBeCloseTo(1.37);
expect(roundToStepPrecision(1.38, 0.25)).toBeCloseTo(1.38);
});
test('should correctly round for step=0.1', () => {
// step=0.1 has 1 decimal place, so it rounds to 1 decimal place
expect(roundToStepPrecision(1.04, 0.1)).toBeCloseTo(1.0);
expect(roundToStepPrecision(1.05, 0.1)).toBeCloseTo(1.1);
expect(roundToStepPrecision(1.14, 0.1)).toBeCloseTo(1.1);
expect(roundToStepPrecision(1.15, 0.1)).toBeCloseTo(1.1); // standard banker's rounding
});
test('should correctly round for step=0.01', () => {
expect(roundToStepPrecision(1.234, 0.01)).toBeCloseTo(1.23);
expect(roundToStepPrecision(1.235, 0.01)).toBeCloseTo(1.24);
expect(roundToStepPrecision(1.236, 0.01)).toBeCloseTo(1.24);
});
});
describe('integration with getDecimalPlaces', () => {
test('should use correct decimal places from step parameter', () => {
// step=0.1 has 1 decimal place
expect(roundToStepPrecision(1.234, 0.1)).toBeCloseTo(1.2);
// step=0.01 has 2 decimal places
expect(roundToStepPrecision(1.234, 0.01)).toBeCloseTo(1.23);
// step=0.001 has 3 decimal places
expect(roundToStepPrecision(1.2345, 0.001)).toBeCloseTo(1.235);
});
test('should handle steps with different precisions correctly', () => {
const value = 1.123456789;
expect(roundToStepPrecision(value, 0.1)).toBeCloseTo(1.1);
expect(roundToStepPrecision(value, 0.01)).toBeCloseTo(1.12);
expect(roundToStepPrecision(value, 0.001)).toBeCloseTo(1.123);
expect(roundToStepPrecision(value, 0.0001)).toBeCloseTo(1.1235);
});
});
describe('return type behavior', () => {
test('should return finite number for valid inputs', () => {
expect(Number.isFinite(roundToStepPrecision(1.23, 0.01))).toBe(true);
});
});
describe('precision edge cases', () => {
test('should round 0.9999 correctly with step=0.01', () => {
expect(roundToStepPrecision(0.9999, 0.01)).toBeCloseTo(1);
});
test('should round 0.99999 correctly with step=0.001', () => {
expect(roundToStepPrecision(0.99999, 0.001)).toBeCloseTo(1);
});
test('should handle rounding up to next integer', () => {
expect(roundToStepPrecision(0.999, 0.001)).toBeCloseTo(0.999);
});
test('should handle values just below step boundary', () => {
expect(roundToStepPrecision(1.4999, 0.01)).toBeCloseTo(1.5);
expect(roundToStepPrecision(1.499, 0.01)).toBeCloseTo(1.5);
});
});
});