diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/search.js b/packages/node_modules/@node-red/editor-client/src/js/ui/search.js index 4ddab7419..d57f05c3a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/search.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/search.js @@ -49,9 +49,13 @@ RED.search = (function() { function indexNode(n) { var l = RED.utils.getNodeLabel(n); if (l) { - l = (""+l).toLowerCase(); - index[l] = index[l] || {}; - index[l][n.id] = {node:n,label:l} + const originalLabel = "" + l; + const indexLabel = originalLabel.toLowerCase(); + index[indexLabel] = index[indexLabel] || {}; + index[indexLabel][n.id] = { + node: n, + label: originalLabel + }; } l = l||n.label||n.name||n.id||""; @@ -683,7 +687,17 @@ RED.search = (function() { show: show, hide: hide, search: search, - getSearchOptions: getSearchOptions + getSearchOptions: getSearchOptions, + // Expose internals for testing + _indexNode: indexNode, + get _index() { return index; }, + set _index(val) { index = val; } }; })(); + + +// Allow CommonJS import for testing +if (typeof module !== "undefined" && module.exports) { + module.exports = RED.search; +} diff --git a/test/unit/@node-red/editor-client/ui/search_spec.js b/test/unit/@node-red/editor-client/ui/search_spec.js new file mode 100644 index 000000000..098810b3c --- /dev/null +++ b/test/unit/@node-red/editor-client/ui/search_spec.js @@ -0,0 +1,162 @@ +const should = require("should"); +const sinon = require("sinon"); + +const NR_TEST_UTILS = require("nr-test-utils"); + +// Path to the search.js module +const searchModulePath = NR_TEST_UTILS.resolve("@node-red/editor-client/src/js/ui/search.js"); + +describe("editor-client/ui/search", function() { + let search; + let mockRED; + + beforeEach(function() { + // Set up minimal RED global mock - only what's needed for tests + mockRED = { + utils: { + getNodeLabel: sinon.stub() + } + }; + global.RED = mockRED; + + // Clear require cache to get fresh module instance + delete require.cache[searchModulePath]; + search = require(searchModulePath); + + // Reset the index for each test + search._index = {}; + }); + + afterEach(function() { + sinon.restore(); + delete global.RED; + delete require.cache[searchModulePath]; + }); + + describe("indexNode", function() { + it("preserves original label casing in search results", function() { + const node = { + id: "node1", + type: "tab", + _def: { category: "config" } + }; + mockRED.utils.getNodeLabel.returns("MyFlow Name"); + + search._indexNode(node); + + // Verify the index key is lowercase (for case-insensitive searching) + should.exist(search._index["myflow name"]); + + // Verify the stored label preserves original casing + search._index["myflow name"]["node1"].label.should.equal("MyFlow Name"); + }); + + it("indexes node with mixed case label correctly", function() { + const node = { + id: "node2", + type: "subflow", + _def: { category: "subflows" } + }; + mockRED.utils.getNodeLabel.returns("MySubFlow_Test"); + + search._indexNode(node); + + // Index key should be lowercase + should.exist(search._index["mysubflow_test"]); + + // Label should preserve original casing + search._index["mysubflow_test"]["node2"].label.should.equal("MySubFlow_Test"); + }); + + it("handles uppercase labels", function() { + const node = { + id: "node3", + type: "tab", + _def: { category: "config" } + }; + mockRED.utils.getNodeLabel.returns("UPPERCASE FLOW"); + + search._indexNode(node); + + should.exist(search._index["uppercase flow"]); + search._index["uppercase flow"]["node3"].label.should.equal("UPPERCASE FLOW"); + }); + + it("handles lowercase labels", function() { + const node = { + id: "node4", + type: "tab", + _def: { category: "config" } + }; + mockRED.utils.getNodeLabel.returns("lowercase flow"); + + search._indexNode(node); + + should.exist(search._index["lowercase flow"]); + search._index["lowercase flow"]["node4"].label.should.equal("lowercase flow"); + }); + + it("stores node reference correctly", function() { + const node = { + id: "node5", + type: "tab", + _def: { category: "config" } + }; + mockRED.utils.getNodeLabel.returns("Test Flow"); + + search._indexNode(node); + + search._index["test flow"]["node5"].node.should.equal(node); + }); + + it("handles nodes without labels by falling back to id", function() { + const node = { + id: "node6", + type: "tab", + _def: { category: "config" } + }; + mockRED.utils.getNodeLabel.returns(null); + + search._indexNode(node); + + // When there's no label from getNodeLabel, + // the node is still indexed by its id + should.exist(search._index["node6"]); + search._index["node6"]["node6"].label.should.equal("node6"); + }); + }); + + describe("search", function() { + it("finds nodes with case-insensitive search", function() { + // Manually set up index with mixed case labels + search._index = { + "myflow": { + "node1": { + node: { id: "node1", type: "tab", _def: { category: "config" } }, + label: "MyFlow" + } + } + }; + + // Search with lowercase should find the node + const results = search.search("myflow"); + results.length.should.equal(1); + results[0].label.should.equal("MyFlow"); + }); + + it("returns preserved casing in search results", function() { + search._index = { + "test subflow": { + "node1": { + node: { id: "node1", type: "subflow", _def: { category: "subflows" } }, + label: "Test SubFlow" + } + } + }; + + const results = search.search("test"); + results.length.should.equal(1); + results[0].label.should.equal("Test SubFlow"); + }); + }); +});