From 39357533b245a90044dfd47fb60e3934e6616281 Mon Sep 17 00:00:00 2001 From: Andrew <159703254+andrewsoonqn@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:00:29 +0800 Subject: [PATCH] Harden speed dial helper inputs --- resources/web/dialog/SpeedDial/speeddial.js | 21 ++++++-------- .../web/dialog/SpeedDial/speeddial.test.js | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 resources/web/dialog/SpeedDial/speeddial.test.js diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 436a8c9a00..ee63c1c204 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -1,5 +1,5 @@ // Speed Dial launcher page. Static-safe module: no DOM access at load time so a -// node vm can exercise the pure helpers (filterActions / parseId / nextSel). +// node vm can exercise the pure helpers (filterActions / actionLabel / nextSel). // ---- state (populated by the C++ bridge via window.HandleStudio) ---- var ACTIONS = []; // [{id,title,source,shortcut}], already frecency-sorted by C++ @@ -36,13 +36,6 @@ function filterActions(actions, query) { return out; } -// why: ids are "script:::"; C++ keys off for dont-ask scoping. -function parseId(id) { - var body = id.slice(id.indexOf(":") + 1); // strip "script:" - var sep = body.indexOf("::"); - return { key: body.slice(0, sep), cap: body.slice(sep + 2) }; -} - function visibleFavourites(favourites, actions) { // why: a fav whose id has no live action (plugin unloaded/disabled) renders a dead // monogram tile whose click run()s to a silent no-op; drop it from the quick-bar. @@ -57,6 +50,10 @@ function resultCountText(total, shown, query) { return (query || "").trim() ? "Showing " + shown + " of " + total + " actions" : total + " actions"; } +function shouldRenderActionList(query) { + return !!(query || "").trim(); +} + // Resolve the selection cursor {zone,i} to the action id it points at: fav zone indexes the // visible favourites, list zone the filtered actions. Pure so runSelected() shares one lookup. function selectedActionId(sel, actions, favIds) { @@ -73,8 +70,8 @@ function prettySource(source) { return String(source || "").toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); }); } -// Accessible label "Title from Pretty Source", disambiguated with the plugin key when another action -// shares the same title+source (case/separator-insensitive) - so two rows never read out identically. +// Accessible label "Title from Pretty Source", disambiguated with the opaque action id when another +// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically. function actionLabel(action, actions) { var label = action.title + " from " + prettySource(action.source); if (actions && actions.length) { @@ -83,7 +80,7 @@ function actionLabel(action, actions) { return o.id !== action.id && foldLabel(o.title) + "|" + foldLabel(o.source) === mine; }); if (clash) - label += " (" + parseId(action.id).key + ")"; + label += " (" + action.id + ")"; } return label; } @@ -327,7 +324,7 @@ function renderList() { listEl.innerHTML = ""; // why: search-first - the list stays blank until the user types; only a // non-empty query with zero hits earns the "No actions match" message. - if (!q) { + if (!shouldRenderActionList(query)) { listEl.className = "dial-list empty"; if (countEl) countEl.hidden = true; return; diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js new file mode 100644 index 0000000000..e29b34f476 --- /dev/null +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -0,0 +1,29 @@ +// Regression tests for the DOM-free Speed Dial helpers. +// Run: node resources/web/dialog/SpeedDial/speeddial.test.js +const assert = require("assert"); +const fs = require("fs"); +const vm = require("vm"); + +const ctx = {}; +ctx.window = ctx; +vm.createContext(ctx); +vm.runInContext(fs.readFileSync(__dirname + "/../../js/fuzzy-search.js", "utf8"), ctx); +vm.runInContext(fs.readFileSync(__dirname + "/speeddial.js", "utf8"), ctx); + +assert.equal(typeof ctx.parseId, "undefined", "opaque action ids must never be parsed"); + +const duplicateActions = [ + { id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" }, + { id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" } +]; +assert.equal( + ctx.actionLabel(duplicateActions[0], duplicateActions), + "Repair from Mesh Tools (0123456789abcdef)", + "duplicate labels should use the opaque id without interpreting its contents" +); + +assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps the action list hidden"); +assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps the action list hidden"); +assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering matching actions"); + +console.log("ok");