// What the styled dropdown's keys and placement do. Run by `npm test`. import assert from "node:assert/strict"; import { test } from "node:test"; import { indexOfValue, moveIndex, nextQuery, placePopup, TYPE_AHEAD_MS, typeAhead, } from "../src/ui/select-field/select-nav.ts"; const opts = ["alpha", "beta", "bravo", "charlie", "Delta"].map((label) => ({ value: label.toLowerCase(), label, })); test("the option holding a value is found, or -1", () => { assert.equal(indexOfValue(opts, "bravo"), 2); assert.equal(indexOfValue(opts, "nope"), -1); }); test("arrow keys step and stop at the ends; Home and End jump", () => { assert.equal(moveIndex(1, 5, "ArrowDown"), 2); assert.equal(moveIndex(4, 5, "ArrowDown"), 4, "stops at the last"); assert.equal(moveIndex(1, 5, "ArrowUp"), 0); assert.equal(moveIndex(0, 5, "ArrowUp"), 0, "stops at the first"); assert.equal(moveIndex(2, 5, "Home"), 0); assert.equal(moveIndex(2, 5, "End"), 4); assert.equal(moveIndex(0, 30, "PageDown"), 10); assert.equal(moveIndex(25, 30, "PageDown"), 29); assert.equal(moveIndex(3, 30, "PageUp"), 0); }); test("a key that moves nothing, or no options, leave the active one alone", () => { assert.equal(moveIndex(2, 5, "a"), 2); assert.equal(moveIndex(0, 0, "ArrowDown"), -1); assert.equal( moveIndex(-1, 3, "ArrowDown"), 0, "from nothing active, the first", ); }); test("typing a letter steps through the options starting with it, wrapping", () => { assert.equal(typeAhead(opts, 0, "b"), 1); assert.equal(typeAhead(opts, 1, "b"), 2); assert.equal(typeAhead(opts, 2, "b"), 1, "wraps round to the first b"); assert.equal(typeAhead(opts, 0, "d"), 4, "case does not matter"); assert.equal(typeAhead(opts, 0, "z"), -1); }); test("the same letter typed again steps on; a longer word narrows in place", () => { assert.equal(typeAhead(opts, 1, "bb"), 2, "bb is b again"); assert.equal(typeAhead(opts, 1, "br"), 2); assert.equal(typeAhead(opts, 2, "br"), 2, "stays while it still matches"); assert.equal(typeAhead(opts, 0, ""), -1); assert.equal(typeAhead([], 0, "a"), -1); }); test("a short pause continues the word, a long one starts a new one", () => { assert.equal(nextQuery("b", "r", 100), "br"); assert.equal(nextQuery("br", "c", TYPE_AHEAD_MS + 1), "c"); }); const viewport = { width: 1000, height: 800 }; test("the list opens under the control when it fits", () => { const p = placePopup( { top: 100, bottom: 140, left: 50, width: 200 }, viewport, 120, ); assert.deepEqual(p, { top: 144, left: 50, width: 200, maxHeight: 120 }); }); test("it opens above when under is too short and above has more room", () => { const p = placePopup( { top: 700, bottom: 740, left: 50, width: 200 }, viewport, 300, ); assert.equal(p.maxHeight, 300); assert.equal(p.top, 700 - 4 - 300); }); test("it scrolls under the control when neither side fits it", () => { const p = placePopup( { top: 100, bottom: 140, left: 50, width: 200 }, { width: 1000, height: 300 }, 900, ); // 300 - 140 - 8 - 4 under; 100 - 12 above: under has more here. assert.equal(p.top, 144); assert.equal(p.maxHeight, 148); }); test("it is at least the minimum width and stays inside the viewport", () => { const p = placePopup( { top: 100, bottom: 140, left: 960, width: 30 }, viewport, 100, 120, ); assert.equal(p.width, 120); assert.equal(p.left, 1000 - 8 - 120); const q = placePopup( { top: 0, bottom: 40, left: -20, width: 100 }, viewport, 50, ); assert.equal(q.left, 8); });