// Unit tests for ParamSlider input math. Verifies modifier-key behaviour
// (Shift = fine, Alt = coarse) for drag / wheel / keyboard paths.
import { describe, expect, it } from 'vitest';
import {
type Range,
clampToRange,
dragModeFrom,
snapToStep,
valueFromAbsolute,
valueFromIncremental,
valueFromKey,
valueFromWheel,
} from '~/ui/sliderInput';
const R: Range = { min: 0, max: 100 };
const R_STEP: Range = { min: 0, max: 100, step: 1 };
describe('dragModeFrom', () => {
it('Shift → fine', () => {
expect(dragModeFrom({ shiftKey: true, altKey: false })).toBe('fine');
});
it('Alt → coarse', () => {
expect(dragModeFrom({ shiftKey: false, altKey: true })).toBe('coarse');
});
it('Shift wins over Alt (browser convention)', () => {
expect(dragModeFrom({ shiftKey: true, altKey: true })).toBe('fine');
});
it('no modifier → absolute', () => {
expect(dragModeFrom({ shiftKey: false, altKey: false })).toBe('absolute');
});
});
describe('snapToStep', () => {
it('snaps to step', () => {
expect(snapToStep(3.7, 1)).toBe(4);
expect(snapToStep(3.4, 1)).toBe(3);
});
it('passes through when step is undefined or 0', () => {
expect(snapToStep(3.7, undefined)).toBe(3.7);
expect(snapToStep(3.7, 0)).toBe(3.7);
});
});
describe('clampToRange', () => {
it('clamps below min and above max', () => {
expect(clampToRange(-5, R)).toBe(0);
expect(clampToRange(120, R)).toBe(100);
expect(clampToRange(42, R)).toBe(42);
});
});
describe('valueFromAbsolute', () => {
it('maps clientX inside track to proportional value', () => {
expect(valueFromAbsolute(100, 0, 200, R)).toBe(50);
expect(valueFromAbsolute(0, 0, 200, R)).toBe(0);
expect(valueFromAbsolute(200, 0, 200, R)).toBe(100);
});
it('clamps outside the track', () => {
expect(valueFromAbsolute(-50, 0, 200, R)).toBe(0);
expect(valueFromAbsolute(500, 0, 200, R)).toBe(100);
});
it('snaps to step', () => {
// 37% of 0..100 = 37; with step=1 it stays 37, but at 37.3% we round to 37.
expect(valueFromAbsolute(37, 0, 100, R_STEP)).toBe(37);
expect(valueFromAbsolute(36.7, 0, 100, R_STEP)).toBe(37);
});
it('returns min when rectWidth is 0 (degenerate layout)', () => {
expect(valueFromAbsolute(50, 0, 0, R)).toBe(0);
});
});
describe('valueFromIncremental', () => {
it('fine mode: full track drag covers 0.1× the range', () => {
// startValue=50, drag full width (200 px) → +0.1 × range = +10
const v = valueFromIncremental({
clientX: 200,
startX: 0,
startValue: 50,
rectWidth: 200,
mode: 'fine',
range: R,
});
expect(v).toBeCloseTo(60, 6);
});
it('coarse mode: full track drag covers 4× the range (clamped)', () => {
const v = valueFromIncremental({
clientX: 200,
startX: 0,
startValue: 50,
rectWidth: 200,
mode: 'coarse',
range: R,
});
// 4× range = 400, but clamped to max 100
expect(v).toBe(100);
});
it('coarse mode: partial drag scales correctly', () => {
const v = valueFromIncremental({
clientX: 10,
startX: 0,
startValue: 50,
rectWidth: 200,
mode: 'coarse',
range: R,
});
// baseRate = 100/200 = 0.5/px; coarse mult = 4; delta = 10*0.5*4 = +20
expect(v).toBeCloseTo(70, 6);
});
it('negative direction works', () => {
const v = valueFromIncremental({
clientX: -20,
startX: 0,
startValue: 50,
rectWidth: 200,
mode: 'fine',
range: R,
});
// baseRate=0.5; fine=0.1; delta = -20*0.5*0.1 = -1
expect(v).toBeCloseTo(49, 6);
});
});
describe('valueFromWheel', () => {
it('default mult = 1 step per notch, scrolls up to increase', () => {
expect(valueFromWheel(50, -100, { shiftKey: false, altKey: false }, R_STEP)).toBe(51);
expect(valueFromWheel(50, 100, { shiftKey: false, altKey: false }, R_STEP)).toBe(49);
});
it('Shift mult = 0.1', () => {
expect(valueFromWheel(50, -100, { shiftKey: true, altKey: false }, R_STEP)).toBe(50);
// step=1 × 0.1 = 0.1; snapped to step=1 → still 50
});
it('Shift mult on a 0.01-step range nudges by 0.001', () => {
const range: Range = { min: 0, max: 1, step: 0.01 };
const v = valueFromWheel(0.5, -1, { shiftKey: true, altKey: false }, range);
// step=0.01 × 0.1 = 0.001; current 0.5 + 0.001 = 0.501; snapped to 0.01 → 0.5
expect(v).toBe(0.5);
// sanity: without modifier we get +0.01
expect(valueFromWheel(0.5, -1, { shiftKey: false, altKey: false }, range)).toBeCloseTo(0.51, 6);
});
it('Alt mult = 10', () => {
expect(valueFromWheel(50, -100, { shiftKey: false, altKey: true }, R_STEP)).toBe(60);
});
it('clamps at boundaries', () => {
expect(valueFromWheel(100, -100, { shiftKey: false, altKey: true }, R_STEP)).toBe(100);
expect(valueFromWheel(0, 100, { shiftKey: false, altKey: true }, R_STEP)).toBe(0);
});
});
describe('valueFromKey', () => {
it('ArrowLeft / ArrowDown decrement', () => {
expect(valueFromKey(50, 'ArrowLeft', { shiftKey: false, altKey: false }, R_STEP)).toBe(49);
expect(valueFromKey(50, 'ArrowDown', { shiftKey: false, altKey: false }, R_STEP)).toBe(49);
});
it('ArrowRight / ArrowUp increment', () => {
expect(valueFromKey(50, 'ArrowRight', { shiftKey: false, altKey: false }, R_STEP)).toBe(51);
expect(valueFromKey(50, 'ArrowUp', { shiftKey: false, altKey: false }, R_STEP)).toBe(51);
});
it('Alt = 10× coarse step', () => {
expect(valueFromKey(50, 'ArrowRight', { shiftKey: false, altKey: true }, R_STEP)).toBe(60);
});
it('Home = min, End = max', () => {
expect(valueFromKey(42, 'Home', { shiftKey: false, altKey: false }, R)).toBe(0);
expect(valueFromKey(42, 'End', { shiftKey: false, altKey: false }, R)).toBe(100);
});
it('unrelated keys return null', () => {
expect(valueFromKey(42, 'Enter', { shiftKey: false, altKey: false }, R)).toBeNull();
expect(valueFromKey(42, 'a', { shiftKey: false, altKey: false }, R)).toBeNull();
});
it('clamps at boundaries', () => {
expect(valueFromKey(100, 'ArrowUp', { shiftKey: false, altKey: false }, R_STEP)).toBe(100);
expect(valueFromKey(0, 'ArrowDown', { shiftKey: false, altKey: false }, R_STEP)).toBe(0);
});
});