153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
clamp,
|
|
centreY,
|
|
formatBytes,
|
|
formatNumber,
|
|
NUMBER_DECIMALS,
|
|
roundTo,
|
|
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 at most three decimals by default and drops trailing zeros', () => {
|
|
expect(NUMBER_DECIMALS).toBe(3);
|
|
expect(formatNumber(100)).toBe('100');
|
|
expect(formatNumber(0.5)).toBe('0.5');
|
|
expect(formatNumber(-12)).toBe('-12');
|
|
expect(formatNumber(9.0909)).toBe('9.091');
|
|
expect(formatNumber(2.1)).toBe('2.1');
|
|
});
|
|
|
|
it('keeps the zeros of a whole number', () => {
|
|
expect(formatNumber(100, 0)).toBe('100');
|
|
expect(formatNumber(1000)).toBe('1000');
|
|
expect(formatNumber(-50, 2)).toBe('-50');
|
|
});
|
|
|
|
it('drops the whole fraction once rounding leaves only zeros', () => {
|
|
expect(formatNumber(2.0004)).toBe('2');
|
|
expect(formatNumber(1.9996)).toBe('2');
|
|
expect(formatNumber(0.10004)).toBe('0.1');
|
|
});
|
|
|
|
it('never hides a fractional value behind a whole number', () => {
|
|
// A step-1 field showing 100.234 as "100" would write the rounded value back on commit.
|
|
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');
|
|
expect(formatNumber(-0.0001)).toBe('0');
|
|
expect(formatNumber(-0.04, 1)).toBe('0');
|
|
});
|
|
|
|
it('honours an explicit precision as an upper bound', () => {
|
|
expect(formatNumber(1.25, 1)).toBe('1.3');
|
|
expect(formatNumber(1.25, 0)).toBe('1');
|
|
expect(formatNumber(1.5, 2)).toBe('1.5');
|
|
expect(formatNumber(12, 2)).toBe('12');
|
|
});
|
|
|
|
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('roundTo', () => {
|
|
it('keeps a value at the precision its field shows', () => {
|
|
// The field rounds, not just formats, so a `decimals: 0` field never shows "641" over a stored 640.5.
|
|
expect(roundTo(640.5, 0)).toBe(641);
|
|
expect(roundTo(32.4, 0)).toBe(32);
|
|
expect(roundTo(1.2345)).toBe(1.234);
|
|
});
|
|
|
|
it('removes binary-float noise', () => {
|
|
expect(roundTo(0.30000000000000004)).toBe(0.3);
|
|
});
|
|
|
|
it('passes non-finite values through', () => {
|
|
expect(roundTo(NaN)).toBeNaN();
|
|
expect(roundTo(Infinity)).toBe(Infinity);
|
|
});
|
|
});
|
|
|
|
describe('snapToStep', () => {
|
|
it('lands on whole numbers for a step-1 field', () => {
|
|
// A scrub adds fractional device pixels; without snapping X would land on 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);
|
|
});
|
|
});
|
|
|
|
describe('formatBytes', () => {
|
|
it('names a small file in bytes, so a sprite never reads as "0 KB"', () => {
|
|
expect(formatBytes(400)).toBe('400 B');
|
|
expect(formatBytes(0)).toBe('0 B');
|
|
});
|
|
|
|
it('switches to KB and MB at the round thresholds', () => {
|
|
expect(formatBytes(1024)).toBe('1 KB');
|
|
expect(formatBytes(1024 * 1024)).toBe('1.0 MB');
|
|
expect(formatBytes(1536 * 1024)).toBe('1.5 MB');
|
|
});
|
|
});
|