import { describe, expect, it } from 'vitest'; import { clamp, centreY, formatNumber, NUMBER_DECIMALS, snapToStep, zoomFactor } from '../../src/ui/math'; describe('clamp', () => { it('keeps a value inside the range', () => { expect(clamp(5, 0, 10)).toBe(5); expect(clamp(-1, 0, 10)).toBe(0); expect(clamp(11, 0, 10)).toBe(10); }); it('lets the lower bound win when the range is inverted', () => { // A popup wider than the screen: pinning it to the left edge beats pinning it to the right. expect(clamp(5, 10, 0)).toBe(10); }); }); describe('centreY', () => { it('centres on whole pixels', () => { expect(centreY(30, 20)).toBe(5); expect(centreY(31, 20)).toBe(6); }); }); describe('formatNumber', () => { it('shows three decimals by default', () => { expect(NUMBER_DECIMALS).toBe(3); expect(formatNumber(100)).toBe('100.000'); expect(formatNumber(0.5)).toBe('0.500'); expect(formatNumber(-12)).toBe('-12.000'); }); it('never hides a fractional value behind a whole number', () => { // The bug this replaces: a field with step 1 showed a stored 100.234 as "100", and // committing that text wrote the rounded value back into the project. expect(formatNumber(100.234)).toBe('100.234'); expect(formatNumber(0.001)).toBe('0.001'); }); it('rounds to the last shown place', () => { expect(formatNumber(1.2345)).toBe('1.234'); expect(formatNumber(1.2356)).toBe('1.236'); }); it('drops the minus sign from a value that displays as zero', () => { expect(formatNumber(-0)).toBe('0.000'); expect(formatNumber(-0.0001)).toBe('0.000'); }); it('honours an explicit precision', () => { expect(formatNumber(1.25, 1)).toBe('1.3'); expect(formatNumber(1.25, 0)).toBe('1'); }); it('shows a field of integral values without a decimal point at all', () => { // What `NumberFieldOptions.decimals: 0` buys: a pixel index reads "33", not "33.000". expect(formatNumber(33, 0)).toBe('33'); expect(formatNumber(-4, 0)).toBe('-4'); // The minus-zero rule holds at every precision, not just the default one. expect(formatNumber(-0.4, 0)).toBe('0'); }); }); describe('snapToStep', () => { it('lands on whole numbers for a step-1 field', () => { // The bug: a scrub added fractional device pixels to an already fractional value, so // dragging X produced values like 100.667 instead of 101. expect(snapToStep(100.667, 1)).toBe(101); expect(snapToStep(100.333, 1)).toBe(100); expect(snapToStep(-3.5, 1)).toBe(-3); }); it('uses the increment as the grid, so a fine drag keeps its precision', () => { // Alt halves the step to 0.1: snapping must not swallow the fine adjustment. expect(snapToStep(1.24, 0.1)).toBe(1.2); expect(snapToStep(1.26, 0.1)).toBe(1.3); expect(snapToStep(3.0000000000000004, 0.1)).toBe(3); }); it('passes values through when there is no grid', () => { expect(snapToStep(1.234, 0)).toBe(1.234); expect(snapToStep(1.234, -1)).toBe(1.234); expect(snapToStep(NaN, 1)).toBeNaN(); }); }); describe('zoomFactor', () => { it('zooms in for a negative delta and out for a positive one', () => { expect(zoomFactor(-100)).toBeGreaterThan(1); expect(zoomFactor(100)).toBeLessThan(1); expect(zoomFactor(0)).toBe(1); }); it('returns exactly to the starting zoom after scrolling back', () => { // Why exponential: the scene view and the graph editor both rely on this cancelling out. expect(1 * zoomFactor(-120) * zoomFactor(120)).toBeCloseTo(1, 12); }); });