From 1d44320f30cb621675db64dd7b9a780fac411c51 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 10 Sep 2026 13:29:57 +0800 Subject: [PATCH] Updated unit tests --- .../web/dialog/SpeedDial/speeddial.test.js | 25 +++++++++++++++ src/slic3r/GUI/ActionRegistry.cpp | 30 +++++++++--------- src/slic3r/GUI/ActionRegistry.hpp | 4 +++ tests/slic3rutils/test_action_source.cpp | 31 +++++++++++++++++++ 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 5b51daffbe..61976ac9b6 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -186,4 +186,29 @@ assert.equal(ctx.revealTarget(100, -5, 50), 50, "negative start is clamped to th assert.equal(ctx.revealTarget(200, 50, 100), 150, "a scroll viewpoint reveals a window past the current rows"); assert.equal(ctx.revealTarget(10, 0, 50), 10, "a list shorter than one window stays fully materialized"); +// visibleFavourites: the quick-bar drops pins whose action no longer exists (plugin unloaded, +// command removed) and collapses duplicate ids, keeping the persisted pin order. +assert.deepEqual(ctx.visibleFavourites(["a", "b", "c"], [{ id: "a" }, { id: "b" }]), + ["a", "b"], "a pin with no live action is dropped from the quick-bar"); +assert.deepEqual(ctx.visibleFavourites(["b", "a", "b"], [{ id: "a" }, { id: "b" }]), + ["b", "a"], "duplicate pins collapse to the first occurrence"); +assert.deepEqual(ctx.visibleFavourites([], [{ id: "a" }]), [], "no pins renders an empty quick-bar"); +assert.deepEqual(ctx.visibleFavourites(["a"], []), [], "a stale config with no actions renders nothing"); + +// tileCode: monogram ladder - title initial, then title+source initials, then a stable ordinal +// by id. The ordinal is keyed by id, not by list order, so frecency reshuffles never renumber tiles. +const monoPool = [ + { id: "z", title: "Repair", source: "Mesh Tools" }, + { id: "a", title: "Repair", source: "Mesh Tools" }, + { id: "b", title: "Repair", source: "Filament" } +]; +assert.equal(ctx.tileCode(monoPool[0], monoPool), "MR2", + "same title+source resolves to source+title initials with an id-keyed ordinal (id z sorts after id a)"); +assert.equal(ctx.tileCode(monoPool[1], monoPool), "MR1", + "the earlier id is numbered first among same-title+source tiles"); +assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR", + "same title but different source resolves to source+title initials"); +assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]), + "S", "a unique title resolves to the bare title initial"); + console.log("ok"); diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index 050814eee3..35503af44e 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -31,6 +31,19 @@ namespace Slic3r { namespace GUI { +std::vector cap_favourites(const std::vector& ids, size_t limit) +{ + std::vector out; + out.reserve(std::min(ids.size(), limit)); + for (const auto& id : ids) { + if (out.size() >= limit) + break; + if (std::find(out.begin(), out.end(), id) == out.end()) + out.push_back(id); + } + return out; +} + namespace { constexpr const char* kConfigSection = "speed_dial"; @@ -468,18 +481,8 @@ bool ActionRegistry::set_favourite(const std::string& id, bool on) std::vector ActionRegistry::favourite_ids() const { assert(wxThread::IsMain()); - // Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, - // even from an older config. The pinned order is intentionally preserved (slice, not sort). - std::vector favs = read_string_array("favourite_actions"); - std::vector out; - out.reserve(std::min(favs.size(), kFavLimit)); - for (const auto& id : favs) { - if (out.size() >= kFavLimit) - break; - if (std::find(out.begin(), out.end(), id) == out.end()) - out.push_back(id); - } - return out; + // Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, even from an older config. + return cap_favourites(read_string_array("favourite_actions"), kFavLimit); } void ActionRegistry::reorder_favourites(const std::vector& ids) @@ -496,8 +499,7 @@ void ActionRegistry::reorder_favourites(const std::vector& ids) if (std::find(next.begin(), next.end(), id) == next.end()) next.push_back(id); // never write the bar back larger than the quick-launch slots - if (next.size() > kFavLimit) - next.resize(kFavLimit); + next = cap_favourites(next, kFavLimit); write_section("favourite_actions", nlohmann::json(next)); } diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index f54b439fb8..2b7af0d132 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -106,6 +106,10 @@ private: std::string m_source_name; // display name of the action's source }; +// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or +// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin. +std::vector cap_favourites(const std::vector& ids, size_t limit); + // Self-contained sink and single owner of runnable actions for the app session. // // Workflow: diff --git a/tests/slic3rutils/test_action_source.cpp b/tests/slic3rutils/test_action_source.cpp index 9b6564ea98..35365c58c5 100644 --- a/tests/slic3rutils/test_action_source.cpp +++ b/tests/slic3rutils/test_action_source.cpp @@ -1,10 +1,13 @@ #include #include "slic3r/GUI/ActionRegistry.hpp" +#include "slic3r/GUI/NativeCommands.hpp" #include +#include #include #include +#include using Slic3r::GUI::AppAction; using Slic3r::GUI::AppActionRunResult; @@ -81,3 +84,31 @@ TEST_CASE("Command actions are keyed by catalog key, not display title", "[speed CHECK(AppAction::compose_id("orca_command", "save_project", "orca") != AppAction::compose_id("orca_command", "load_project", "orca")); } + +// The quick-launch cap must stay 10 to match the numbered Alt/Option+1..9,0 keys. The web palette +// mirrors it as K_FAV_LIMIT (asserted in speeddial.test.js); the C++ side pins it here. +static_assert(Slic3r::GUI::ActionRegistry::kFavLimit == 10, "kFavLimit must stay 10"); + +TEST_CASE("Favourite lists are capped and deduped preserving order", "[speeddial][actions]") +{ + using Slic3r::GUI::cap_favourites; + + CHECK(cap_favourites({}, 10) == std::vector{}); + CHECK(cap_favourites({"a", "b", "a"}, 10) == std::vector{"a", "b"}); + CHECK(cap_favourites({"c", "a", "b", "c"}, 3) == std::vector{"c", "a", "b"}); + CHECK(cap_favourites({"a", "b"}, 0) == std::vector{}); +} + +TEST_CASE("Native command catalog has unique keys and present titles", "[speeddial][actions]") +{ + const std::vector& commands = Slic3r::GUI::NativeCommands::catalog(); + CHECK_FALSE(commands.empty()); + + std::set seen; + for (const auto& c : commands) { + CHECK_FALSE(c.key.empty()); + CHECK_FALSE(c.title.empty()); + // A duplicated key would silently shadow the earlier command in the palette. + CHECK(seen.insert(c.key).second); + } +}