utils/web_ui/src/stepped-value.test.ts
Krzysztof kuhy Rudnicki c5b3fceeae ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config
The two shared packages are the first repos on the preset (link:../ts_config
inside the monorepo; consumers outside use the git tag). Each eslint.config.js
is now the defineConfig() call plus one override with its reason: a public
API name (`nextChance`, `fuzzyMatch`) the boolean-prefix rule would rename,
which is a breaking change for the three repos importing it.

Everything else the preset flagged is fixed in code, not disabled:
key order (perfectionist), hex casing, sort compare functions in tests,
Iterator#map().toArray(), explicit returns on multiline arrows.

web_ui: react-refresh/only-export-components was a real finding --
range-slider.tsx exported `steppedValue` next to the component, which breaks
Vite HMR for the module. The keyboard contract moves to stepped-value.ts as
`steppedValue` + a new pure `keyTarget(key, values, current)`, which also
resolves unicorn/no-late-event-control: the handler now decides the target
first and calls preventDefault() only when it owns the key. Its tests move
with it (the 250-line gate caught the combined file). Minor bump for the new
export. dist/ rebuilt in both packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnAYyNH1Sm4iJNidfYbrZJ
2026-09-16 21:46:41 +02:00

80 lines
2.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { keyTarget, steppedValue } from "./stepped-value.ts";
/** Ten evenly spaced values, so a quantile fraction maps to a predictable value. */
const VALUES = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90];
describe("keyTarget", () => {
it("leaves keys the slider does not own to the browser", () => {
expect(keyTarget("Tab", VALUES, 30)).toBeUndefined();
expect(keyTarget("a", VALUES, 30)).toBeUndefined();
});
it("maps every owned key onto the distribution", () => {
expect(keyTarget("ArrowLeft", VALUES, 30)).toBe(20);
expect(keyTarget("ArrowDown", VALUES, 30)).toBe(20);
expect(keyTarget("ArrowRight", VALUES, 30)).toBe(40);
expect(keyTarget("ArrowUp", VALUES, 30)).toBe(40);
expect(keyTarget("Home", VALUES, 30)).toBe(0);
expect(keyTarget("End", VALUES, 30)).toBe(90);
});
});
describe("steppedValue", () => {
it("advances one sample at a time", () => {
expect(steppedValue(VALUES, 30, 1)).toBe(40);
});
it("moves backwards for a negative delta", () => {
expect(steppedValue(VALUES, 30, -1)).toBe(20);
});
it("clamps at the top", () => {
expect(steppedValue(VALUES, 90, 5)).toBe(90);
});
it("clamps at the bottom", () => {
expect(steppedValue(VALUES, 0, -5)).toBe(0);
});
it("rounds a between-samples value to a real sample", () => {
expect(steppedValue(VALUES, 34, 1)).toBe(40);
});
it("returns the current sample for a zero delta", () => {
expect(steppedValue(VALUES, 30, 0)).toBe(30);
});
describe("on a lumpy distribution", () => {
// The shape that actually broke in the browser: awesome-mcp-explorer's
// star counts hold ~1500 duplicate zeros, so an index step inside the run
// moved the index but not the value, and the arrow key looked dead.
const LUMPY = [...Array.from({ length: 50 }, () => 0), 5, 5, 5, 100, 5000];
it("escapes a run of duplicates instead of stalling", () => {
expect(steppedValue(LUMPY, 0, 1)).toBe(5);
});
it("keeps stepping past the second run", () => {
expect(steppedValue(LUMPY, 5, 1)).toBe(100);
});
it("steps back out of a run", () => {
expect(steppedValue(LUMPY, 5, -1)).toBe(0);
});
it("counts distinct values for a multi-step jump", () => {
expect(steppedValue(LUMPY, 0, 2)).toBe(100);
});
it("clamps at the top rather than running off the end", () => {
expect(steppedValue(LUMPY, 5000, 1)).toBe(5000);
});
it("clamps at the bottom", () => {
expect(steppedValue(LUMPY, 0, -1)).toBe(0);
});
});
});