Zum Inhalt

tests/ui/sliderInput.test.ts

// 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);
  });
});