From a55aa17939642adc6bbe17d123b56cb0aec54943 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Tue, 30 Dec 2025 10:43:59 -0800 Subject: [PATCH 1/5] Fix search dialog to preserve flow/subflow name casing & add unit tests --- .../editor-client/src/js/ui/search.js | 22 +- .../@node-red/editor-client/ui/search_spec.js | 190 ++++++++++++++++++ 2 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 test/unit/@node-red/editor-client/ui/search_spec.js 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..cab4bf5d7 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; +} \ No newline at end of file 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..ae63577ea --- /dev/null +++ b/test/unit/@node-red/editor-client/ui/search_spec.js @@ -0,0 +1,190 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +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"); + }); + }); +}); From 58c4fd585864bd224d30958bf80bf6fa49a54492 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Tue, 30 Dec 2025 10:48:23 -0800 Subject: [PATCH 2/5] Fix indenting with 4-spaces since there can be no arguments --- .../@node-red/editor-client/ui/search_spec.js | 292 +++++++++--------- 1 file changed, 140 insertions(+), 152 deletions(-) diff --git a/test/unit/@node-red/editor-client/ui/search_spec.js b/test/unit/@node-red/editor-client/ui/search_spec.js index ae63577ea..f23ada673 100644 --- a/test/unit/@node-red/editor-client/ui/search_spec.js +++ b/test/unit/@node-red/editor-client/ui/search_spec.js @@ -20,171 +20,159 @@ 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" -); +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; +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; + 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); + // 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"); + // Reset the index for each test + search._index = {}; }); - 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" - ); + afterEach(function() { + sinon.restore(); + delete global.RED; + delete require.cache[searchModulePath]; }); - it("handles uppercase labels", function () { - const node = { - id: "node3", - type: "tab", - _def: { category: "config" }, - }; - mockRED.utils.getNodeLabel.returns("UPPERCASE FLOW"); + 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); + search._indexNode(node); - should.exist(search._index["uppercase flow"]); - search._index["uppercase flow"]["node3"].label.should.equal( - "UPPERCASE FLOW" - ); + // 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"); + }); }); - it("handles lowercase labels", function () { - const node = { - id: "node4", - type: "tab", - _def: { category: "config" }, - }; - mockRED.utils.getNodeLabel.returns("lowercase flow"); + 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._indexNode(node); + // Search with lowercase should find the node + const results = search.search("myflow"); + results.length.should.equal(1); + results[0].label.should.equal("MyFlow"); + }); - should.exist(search._index["lowercase flow"]); - search._index["lowercase flow"]["node4"].label.should.equal( - "lowercase flow" - ); + 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"); + }); }); - - 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"); - }); - }); }); From 0689306569ebab9236417153f105da80346ac779 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Tue, 30 Dec 2025 10:55:54 -0800 Subject: [PATCH 3/5] Add new line at end of file --- .../node_modules/@node-red/editor-client/src/js/ui/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cab4bf5d7..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 @@ -700,4 +700,4 @@ RED.search = (function() { // Allow CommonJS import for testing if (typeof module !== "undefined" && module.exports) { module.exports = RED.search; -} \ No newline at end of file +} From 7b62e06a49d2c007673b5db0cbd4788fe5bc8f1b Mon Sep 17 00:00:00 2001 From: Noley Holland <63617269+n-lark@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:55:42 -0800 Subject: [PATCH 5/5] Update test/unit/@node-red/editor-client/ui/search_spec.js to remove copyright Co-authored-by: Nick O'Leary --- .../@node-red/editor-client/ui/search_spec.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/unit/@node-red/editor-client/ui/search_spec.js b/test/unit/@node-red/editor-client/ui/search_spec.js index f23ada673..098810b3c 100644 --- a/test/unit/@node-red/editor-client/ui/search_spec.js +++ b/test/unit/@node-red/editor-client/ui/search_spec.js @@ -1,19 +1,3 @@ -/** - * Copyright JS Foundation and other contributors, http://js.foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - **/ - const should = require("should"); const sinon = require("sinon");