From 2b9eb159ee48724b4065c3ee6190756e0051bf57 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 29 Jun 2025 23:41:53 +0530 Subject: [PATCH 1/8] feat: collapse all button in the sidebar which closes all the open directories --- .../CollapseFolders/main.js | 79 +++++++++++++++++++ src/extensionsIntegrated/loader.js | 1 + src/styles/Extn-CollapseFolders.less | 24 ++++++ src/styles/brackets.less | 1 + 4 files changed, 105 insertions(+) create mode 100644 src/extensionsIntegrated/CollapseFolders/main.js create mode 100644 src/styles/Extn-CollapseFolders.less diff --git a/src/extensionsIntegrated/CollapseFolders/main.js b/src/extensionsIntegrated/CollapseFolders/main.js new file mode 100644 index 0000000000..bc11173398 --- /dev/null +++ b/src/extensionsIntegrated/CollapseFolders/main.js @@ -0,0 +1,79 @@ +define(function (require, exports, module) { + const AppInit = require("utils/AppInit"); + const ProjectManager = require("project/ProjectManager"); + + /** + * This is the main function that handles the closing of all the directories + */ + function handleCollapseBtnClick() { + // this will give us an array of array's + // the root level directories will be at index 0, its next level will be at index 1 and so on + const openNodes = ProjectManager._actionCreator.model.getOpenNodes(); + if (!openNodes || openNodes.length === 0) { + return; + } + + // traversing from the back because the deepest nested directories should be closed first + // Note: this is an array of all the directories at the deepest level + for (let i = openNodes.length - 1; i >= 0; i--) { + // close all the directories + openNodes[i].forEach(function (folderPath) { + try { + // to close each dir + ProjectManager._actionCreator.setDirectoryOpen(folderPath, false); + } catch (error) { + console.error("Failed to close folder:", folderPath, error); + } + }); + } + } + + /** + * This function is responsible to create the 'Collapse All' button + * and append it to the sidebar area on the project-files-header + */ + function createCollapseButton() { + const $projectFilesHeader = $("#project-files-header"); + // make sure that we were able to get the project-files-header DOM element + if ($projectFilesHeader.length === 0) { + return; + } + + // create the collapse btn + const $collapseBtn = $(` +
+ + +
+ `); + + $collapseBtn.on("click", handleCollapseBtnClick); + + $projectFilesHeader.append($collapseBtn); // append the btn to the project-files-header + + _setupHoverBehavior($collapseBtn); // hover functionality to show/hide the button + } + + /** + * This function is responsible for the hover behavior to show/hide the collapse button + * we only show the button when the cursor is over the sidebar area + */ + function _setupHoverBehavior($collapseBtn) { + const $sidebar = $("#sidebar"); + if ($sidebar.length === 0) { + return; + } + + $sidebar.on("mouseenter", function () { + $collapseBtn.addClass("show"); + }); + + $sidebar.on("mouseleave", function () { + $collapseBtn.removeClass("show"); + }); + } + + AppInit.appReady(function () { + createCollapseButton(); + }); +}); diff --git a/src/extensionsIntegrated/loader.js b/src/extensionsIntegrated/loader.js index 776a81dd0f..523db814bb 100644 --- a/src/extensionsIntegrated/loader.js +++ b/src/extensionsIntegrated/loader.js @@ -45,4 +45,5 @@ define(function (require, exports, module) { require("./CSSColorPreview/main"); require("./TabBar/main"); require("./CustomSnippets/main"); + require("./CollapseFolders/main"); }); diff --git a/src/styles/Extn-CollapseFolders.less b/src/styles/Extn-CollapseFolders.less new file mode 100644 index 0000000000..62ebbc9e74 --- /dev/null +++ b/src/styles/Extn-CollapseFolders.less @@ -0,0 +1,24 @@ +#collapse-folders { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0.2em 0.4em; + position: absolute !important; + right: 0.5em; + opacity: 0; + visibility: hidden; + transition: + opacity 0.2s ease-in-out, + visibility 0.2s ease-in-out; + + .collapse-icon { + font-size: 0.5em; + line-height: 1; + } + + &.show { + opacity: 1; + visibility: visible; + } +} diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 5150753c88..f7100dee51 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -45,6 +45,7 @@ @import "Extn-DisplayShortcuts.less"; @import "Extn-CSSColorPreview.less"; @import "Extn-CustomSnippets.less"; +@import "Extn-CollapseFolders.less"; @import "UserProfile.less"; /* Overall layout */ From 73d2917fbad802e70d8eb48289764373d0eb1aad Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 29 Jun 2025 23:46:36 +0530 Subject: [PATCH 2/8] chore: add license --- .../CollapseFolders/main.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/extensionsIntegrated/CollapseFolders/main.js b/src/extensionsIntegrated/CollapseFolders/main.js index bc11173398..02109ea680 100644 --- a/src/extensionsIntegrated/CollapseFolders/main.js +++ b/src/extensionsIntegrated/CollapseFolders/main.js @@ -1,3 +1,26 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* Displays a Collapse button in the sidebar area */ +/* when the button gets clicked, it closes all the directories recursively that are opened */ +/* Styling for the button is done in `../../styles/Extn-CollapseFolders.less` */ define(function (require, exports, module) { const AppInit = require("utils/AppInit"); const ProjectManager = require("project/ProjectManager"); From 1411a66705c71886fa7a15e8744b91536926420e Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 30 Jun 2025 17:20:54 +0530 Subject: [PATCH 3/8] refactor: update button styling to make it consistent with rest of UI --- src/styles/Extn-CollapseFolders.less | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/styles/Extn-CollapseFolders.less b/src/styles/Extn-CollapseFolders.less index 62ebbc9e74..7c96b7bf22 100644 --- a/src/styles/Extn-CollapseFolders.less +++ b/src/styles/Extn-CollapseFolders.less @@ -3,9 +3,10 @@ flex-direction: column; align-items: center; justify-content: center; - padding: 0.2em 0.4em; + padding: 0.2em 0.65em; + margin-top: 0.1em; position: absolute !important; - right: 0.5em; + right: 0; opacity: 0; visibility: hidden; transition: From 886cc5b8a00a2c6748b18ce1bb7a8c6ddbbd5096 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 30 Jun 2025 17:34:10 +0530 Subject: [PATCH 4/8] fix: button not appearing when cursor is already over sidebar on site load --- src/extensionsIntegrated/CollapseFolders/main.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/extensionsIntegrated/CollapseFolders/main.js b/src/extensionsIntegrated/CollapseFolders/main.js index 02109ea680..6efdd2962f 100644 --- a/src/extensionsIntegrated/CollapseFolders/main.js +++ b/src/extensionsIntegrated/CollapseFolders/main.js @@ -87,6 +87,12 @@ define(function (require, exports, module) { return; } + // we need this check to see if on site load the mouse is already over the sidebar area or not + // because if it is, then we need to show the button + if ($sidebar.is(":hover")) { + $collapseBtn.addClass("show"); + } + $sidebar.on("mouseenter", function () { $collapseBtn.addClass("show"); }); From 67f945caebf55266fcc6b33b3041fde64192b28b Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 30 Jun 2025 17:54:15 +0530 Subject: [PATCH 5/8] fix: collapse and git button overlapping issue --- src/extensions/default/Git/styles/git-styles.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/default/Git/styles/git-styles.less b/src/extensions/default/Git/styles/git-styles.less index 534fbc1d69..4b98e72795 100644 --- a/src/extensions/default/Git/styles/git-styles.less +++ b/src/extensions/default/Git/styles/git-styles.less @@ -907,6 +907,7 @@ white-space: nowrap; padding: 2px 5px; margin-left: -5px; + margin-right: 2em; .dropdown-arrow { display: inline-block; width: 7px; From 8a5eb9a7526c7ef9b497f0a103aa9416e792b91a Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 4 Jul 2025 13:17:26 +0530 Subject: [PATCH 6/8] feat: integ tests for collapse folders feature --- test/UnitTestSuite.js | 1 + test/spec/Extn-CollapseFolders-integ-test.js | 334 +++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 test/spec/Extn-CollapseFolders-integ-test.js diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index 4fd8b5221d..0afbbd1b16 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -128,6 +128,7 @@ define(function (require, exports, module) { require("spec/Extn-JSHint-integ-test"); require("spec/Extn-ESLint-integ-test"); require("spec/Extn-CSSColorPreview-integ-test"); + require("spec/Extn-CollapseFolders-integ-test"); // extension integration tests require("spec/Extn-CSSCodeHints-integ-test"); require("spec/Extn-HTMLCodeHints-Lint-integ-test"); diff --git a/test/spec/Extn-CollapseFolders-integ-test.js b/test/spec/Extn-CollapseFolders-integ-test.js new file mode 100644 index 0000000000..115c63fdfe --- /dev/null +++ b/test/spec/Extn-CollapseFolders-integ-test.js @@ -0,0 +1,334 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone */ + +define(function (require, exports, module) { + const SpecRunnerUtils = require("spec/SpecRunnerUtils"); + + const testPath = SpecRunnerUtils.getTestPath("/spec/ProjectManager-test-files"); + + let ProjectManager, // loaded from brackets.test + CommandManager, // loaded from brackets.test + testWindow, + brackets, + $; + + describe("integration:CollapseFolders", function () { + beforeAll(async function () { + testWindow = await SpecRunnerUtils.createTestWindowAndRun(); + brackets = testWindow.brackets; + ProjectManager = brackets.test.ProjectManager; + CommandManager = brackets.test.CommandManager; + $ = testWindow.$; + + await SpecRunnerUtils.loadProjectInTestWindow(testPath); + }, 30000); + + afterAll(async function () { + ProjectManager = null; + CommandManager = null; + testWindow = null; + brackets = null; + $ = null; + await SpecRunnerUtils.closeTestWindow(); + }, 30000); + + afterEach(async function () { + await testWindow.closeAllFiles(); + }); + + it("Should create collapse button in project files header", function () { + const $projectFilesHeader = $("#project-files-header"); + const $collapseBtn = $("#collapse-folders"); + + expect($projectFilesHeader.length).toBe(1); + expect($collapseBtn.length).toBe(1); + expect($collapseBtn.parent()[0]).toBe($projectFilesHeader[0]); + }); + + it("Should have correct button structure and classes", function () { + const $collapseBtn = $("#collapse-folders"); + const $icons = $collapseBtn.find("i.collapse-icon"); + + expect($collapseBtn.hasClass("btn-alt-quiet")).toBe(true); + expect($collapseBtn.attr("title")).toBe("Collapse All"); + expect($icons.length).toBe(2); + expect($icons.eq(0).hasClass("fa-solid")).toBe(true); + expect($icons.eq(0).hasClass("fa-chevron-down")).toBe(true); + expect($icons.eq(1).hasClass("fa-solid")).toBe(true); + expect($icons.eq(1).hasClass("fa-chevron-up")).toBe(true); + }); + + it("Should show button on sidebar hover", async function () { + const $sidebar = $("#sidebar"); + const $collapseBtn = $("#collapse-folders"); + + // Initially button should not have show class + expect($collapseBtn.hasClass("show")).toBe(false); + + // Trigger mouseenter on sidebar + $sidebar.trigger("mouseenter"); + + await awaitsFor( + function () { + return $collapseBtn.hasClass("show"); + }, + "Button should show on sidebar hover", + 1000 + ); + + expect($collapseBtn.hasClass("show")).toBe(true); + }); + + it("Should hide button on sidebar mouse leave", async function () { + const $sidebar = $("#sidebar"); + const $collapseBtn = $("#collapse-folders"); + + // First show the button + $sidebar.trigger("mouseenter"); + await awaitsFor( + function () { + return $collapseBtn.hasClass("show"); + }, + "Button should show first", + 1000 + ); + + // Then trigger mouseleave + $sidebar.trigger("mouseleave"); + + await awaitsFor( + function () { + return !$collapseBtn.hasClass("show"); + }, + "Button should hide on sidebar mouse leave", + 1000 + ); + + expect($collapseBtn.hasClass("show")).toBe(false); + }); + + it("Should have click handler attached", function () { + const $collapseBtn = $("#collapse-folders"); + const events = $._data($collapseBtn[0], "events"); + + expect(events).toBeTruthy(); + expect(events.click).toBeTruthy(); + expect(events.click.length).toBe(1); + }); + + function findTreeNode(fullPath) { + const $treeItems = testWindow.$("#project-files-container li"); + let $result; + + const name = fullPath.split("/").pop(); + + $treeItems.each(function () { + const $treeNode = testWindow.$(this); + if ($treeNode.children("a").text().trim() === name) { + $result = $treeNode; + return false; // break the loop + } + }); + return $result; + } + + async function openFolder(folderPath) { + const $treeNode = findTreeNode(folderPath); + expect($treeNode).toBeTruthy(); + + if (!$treeNode.hasClass("jstree-open")) { + $treeNode.children("a").children("span").click(); + + await awaitsFor( + function () { + return $treeNode.hasClass("jstree-open"); + }, + `Open folder ${folderPath}`, + 2000 + ); + } + } + + function isFolderOpen(folderPath) { + const $treeNode = findTreeNode(folderPath); + return $treeNode && $treeNode.hasClass("jstree-open"); + } + + function getOpenFolders() { + const openFolders = []; + testWindow.$("#project-files-container li.jstree-open").each(function () { + const $node = testWindow.$(this); + const folderName = $node.children("a").text().trim(); + if (folderName) { + openFolders.push(folderName); + } + }); + return openFolders; + } + + it("Should collapse all open directories when clicked", async function () { + // First, open some directories + const directoryPath = testPath + "/directory"; + + await openFolder("directory"); + + // Verify the directory is open + expect(isFolderOpen("directory")).toBe(true); + + // Show the collapse button by hovering over sidebar + const $sidebar = $("#sidebar"); + const $collapseBtn = $("#collapse-folders"); + $sidebar.trigger("mouseenter"); + + await awaitsFor( + function () { + return $collapseBtn.hasClass("show"); + }, + "Button should show", + 1000 + ); + + // Click the collapse button + $collapseBtn.trigger("click"); + + // Wait for directories to close + await awaitsFor( + function () { + return !isFolderOpen("directory"); + }, + "Directory should be closed after clicking collapse button", + 2000 + ); + + // Verify the directory is now closed + expect(isFolderOpen("directory")).toBe(false); + }); + + it("Should collapse multiple open directories when clicked", async function () { + // Open multiple directories if they exist + await openFolder("directory"); + + // Verify directories are open + expect(isFolderOpen("directory")).toBe(true); + + const initialOpenFolders = getOpenFolders(); + expect(initialOpenFolders.length).toBeGreaterThan(0); + + // Show the collapse button and click it + const $sidebar = $("#sidebar"); + const $collapseBtn = $("#collapse-folders"); + $sidebar.trigger("mouseenter"); + + await awaitsFor( + function () { + return $collapseBtn.hasClass("show"); + }, + "Button should show", + 1000 + ); + + $collapseBtn.trigger("click"); + + // Wait for all directories to close + await awaitsFor( + function () { + return getOpenFolders().length === 0; + }, + "All directories should be closed", + 2000 + ); + + // Verify no directories are open + expect(getOpenFolders().length).toBe(0); + }); + + it("Should handle click when no directories are open", function () { + // Ensure no directories are open initially + const openFolders = getOpenFolders(); + expect(openFolders.length).toBe(0); + + // Show the collapse button and click it + const $sidebar = $("#sidebar"); + const $collapseBtn = $("#collapse-folders"); + $sidebar.trigger("mouseenter"); + + // This should not throw an error + expect(function () { + $collapseBtn.trigger("click"); + }).not.toThrow(); + + // Should still have no open folders + expect(getOpenFolders().length).toBe(0); + }); + + it("Should work with nested directories", async function () { + // Open a parent directory first + await openFolder("directory"); + expect(isFolderOpen("directory")).toBe(true); + + // If there are subdirectories, try to open one + // Note: This test assumes the test project has nested directories + const $subdirs = testWindow.$("#project-files-container li.jstree-open li.jstree-closed"); + if ($subdirs.length > 0) { + // Open a subdirectory if one exists + $subdirs.first().children("a").children("span").click(); + + await awaitsFor( + function () { + return $subdirs.first().hasClass("jstree-open"); + }, + "Open subdirectory", + 2000 + ); + } + + const initialOpenCount = getOpenFolders().length; + expect(initialOpenCount).toBeGreaterThan(0); + + // Show the collapse button and click it + const $sidebar = $("#sidebar"); + const $collapseBtn = $("#collapse-folders"); + $sidebar.trigger("mouseenter"); + + await awaitsFor( + function () { + return $collapseBtn.hasClass("show"); + }, + "Button should show", + 1000 + ); + + $collapseBtn.trigger("click"); + + // Wait for all directories to close (including nested ones) + await awaitsFor( + function () { + return getOpenFolders().length === 0; + }, + "All nested directories should be closed", + 2000 + ); + + expect(getOpenFolders().length).toBe(0); + }); + }); +}); From 4d473fc13f5c67a4c98ae9cb082319d07feb1fd4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 4 Jul 2025 17:38:18 +0530 Subject: [PATCH 7/8] fix: remove redundant JS for button visibility and replaced with css --- .../CollapseFolders/main.js | 28 ------------------- src/styles/Extn-CollapseFolders.less | 8 +++--- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/src/extensionsIntegrated/CollapseFolders/main.js b/src/extensionsIntegrated/CollapseFolders/main.js index 6efdd2962f..98dd4f3bbb 100644 --- a/src/extensionsIntegrated/CollapseFolders/main.js +++ b/src/extensionsIntegrated/CollapseFolders/main.js @@ -71,35 +71,7 @@ define(function (require, exports, module) { `); $collapseBtn.on("click", handleCollapseBtnClick); - $projectFilesHeader.append($collapseBtn); // append the btn to the project-files-header - - _setupHoverBehavior($collapseBtn); // hover functionality to show/hide the button - } - - /** - * This function is responsible for the hover behavior to show/hide the collapse button - * we only show the button when the cursor is over the sidebar area - */ - function _setupHoverBehavior($collapseBtn) { - const $sidebar = $("#sidebar"); - if ($sidebar.length === 0) { - return; - } - - // we need this check to see if on site load the mouse is already over the sidebar area or not - // because if it is, then we need to show the button - if ($sidebar.is(":hover")) { - $collapseBtn.addClass("show"); - } - - $sidebar.on("mouseenter", function () { - $collapseBtn.addClass("show"); - }); - - $sidebar.on("mouseleave", function () { - $collapseBtn.removeClass("show"); - }); } AppInit.appReady(function () { diff --git a/src/styles/Extn-CollapseFolders.less b/src/styles/Extn-CollapseFolders.less index 7c96b7bf22..ecdfaab1ca 100644 --- a/src/styles/Extn-CollapseFolders.less +++ b/src/styles/Extn-CollapseFolders.less @@ -17,9 +17,9 @@ font-size: 0.5em; line-height: 1; } +} - &.show { - opacity: 1; - visibility: visible; - } +#sidebar:hover #collapse-folders { + opacity: 1; + visibility: visible; } From d81239cb805c1fd4017b034db068373381f807ee Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 4 Jul 2025 21:51:19 +0530 Subject: [PATCH 8/8] fix: integ tests rewrite --- test/spec/Extn-CollapseFolders-integ-test.js | 419 ++++++++----------- 1 file changed, 163 insertions(+), 256 deletions(-) diff --git a/test/spec/Extn-CollapseFolders-integ-test.js b/test/spec/Extn-CollapseFolders-integ-test.js index 115c63fdfe..b4bec85493 100644 --- a/test/spec/Extn-CollapseFolders-integ-test.js +++ b/test/spec/Extn-CollapseFolders-integ-test.js @@ -18,317 +18,224 @@ * */ -/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone */ +/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, awaits, jsPromise */ define(function (require, exports, module) { const SpecRunnerUtils = require("spec/SpecRunnerUtils"); - const testPath = SpecRunnerUtils.getTestPath("/spec/ProjectManager-test-files"); + describe("integration:Collapse Folders", function () { + let testWindow, ProjectManager, FileSystem, $, testProjectPath, testProjectFolder; - let ProjectManager, // loaded from brackets.test - CommandManager, // loaded from brackets.test - testWindow, - brackets, - $; - - describe("integration:CollapseFolders", function () { beforeAll(async function () { + // Create the test window testWindow = await SpecRunnerUtils.createTestWindowAndRun(); - brackets = testWindow.brackets; - ProjectManager = brackets.test.ProjectManager; - CommandManager = brackets.test.CommandManager; + // Get reference to useful modules $ = testWindow.$; + ProjectManager = testWindow.brackets.test.ProjectManager; + FileSystem = testWindow.brackets.test.FileSystem; + + // Setup a test project folder with nested directories + testProjectPath = SpecRunnerUtils.getTempDirectory() + "/collapse-folders-test"; + testProjectFolder = FileSystem.getDirectoryForPath(testProjectPath); + + // Ensure the test directory exists + await SpecRunnerUtils.createTempDirectory(); + + // Create test project structure + await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath); + await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder1"); + await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder1/subfolder1"); + await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder2"); + await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder2/subfolder2"); + + // Create some test files + await jsPromise(SpecRunnerUtils.createTextFile(testProjectPath + "/file.js", "// Test file", FileSystem)); + await jsPromise( + SpecRunnerUtils.createTextFile(testProjectPath + "/folder1/file1.js", "// Test file 1", FileSystem) + ); + await jsPromise( + SpecRunnerUtils.createTextFile( + testProjectPath + "/folder1/subfolder1/subfile1.js", + "// Test subfile 1", + FileSystem + ) + ); + await jsPromise( + SpecRunnerUtils.createTextFile(testProjectPath + "/folder2/file2.js", "// Test file 2", FileSystem) + ); + await jsPromise( + SpecRunnerUtils.createTextFile( + testProjectPath + "/folder2/subfolder2/subfile2.js", + "// Test subfile 2", + FileSystem + ) + ); - await SpecRunnerUtils.loadProjectInTestWindow(testPath); + // Load the test project + await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath); }, 30000); afterAll(async function () { - ProjectManager = null; - CommandManager = null; testWindow = null; - brackets = null; - $ = null; await SpecRunnerUtils.closeTestWindow(); + await SpecRunnerUtils.removeTempDirectory(); }, 30000); - afterEach(async function () { - await testWindow.closeAllFiles(); - }); - - it("Should create collapse button in project files header", function () { - const $projectFilesHeader = $("#project-files-header"); - const $collapseBtn = $("#collapse-folders"); - - expect($projectFilesHeader.length).toBe(1); - expect($collapseBtn.length).toBe(1); - expect($collapseBtn.parent()[0]).toBe($projectFilesHeader[0]); - }); - - it("Should have correct button structure and classes", function () { - const $collapseBtn = $("#collapse-folders"); - const $icons = $collapseBtn.find("i.collapse-icon"); - - expect($collapseBtn.hasClass("btn-alt-quiet")).toBe(true); - expect($collapseBtn.attr("title")).toBe("Collapse All"); - expect($icons.length).toBe(2); - expect($icons.eq(0).hasClass("fa-solid")).toBe(true); - expect($icons.eq(0).hasClass("fa-chevron-down")).toBe(true); - expect($icons.eq(1).hasClass("fa-solid")).toBe(true); - expect($icons.eq(1).hasClass("fa-chevron-up")).toBe(true); - }); - - it("Should show button on sidebar hover", async function () { - const $sidebar = $("#sidebar"); - const $collapseBtn = $("#collapse-folders"); - - // Initially button should not have show class - expect($collapseBtn.hasClass("show")).toBe(false); - - // Trigger mouseenter on sidebar - $sidebar.trigger("mouseenter"); - - await awaitsFor( - function () { - return $collapseBtn.hasClass("show"); - }, - "Button should show on sidebar hover", - 1000 - ); - - expect($collapseBtn.hasClass("show")).toBe(true); - }); - - it("Should hide button on sidebar mouse leave", async function () { - const $sidebar = $("#sidebar"); - const $collapseBtn = $("#collapse-folders"); - - // First show the button - $sidebar.trigger("mouseenter"); - await awaitsFor( - function () { - return $collapseBtn.hasClass("show"); - }, - "Button should show first", - 1000 - ); - - // Then trigger mouseleave - $sidebar.trigger("mouseleave"); + /** + * Helper function to open a folder in the project tree + * @param {string} folderPath - The path of the folder to open + */ + async function openFolder(folderPath) { + const folderEntry = FileSystem.getDirectoryForPath(folderPath); + // Call setDirectoryOpen without awaitsForDone since it doesn't return a promise + ProjectManager._actionCreator.setDirectoryOpen(folderEntry.fullPath, true); + // Wait for the folder to be opened in the UI await awaitsFor( function () { - return !$collapseBtn.hasClass("show"); + const $folderNode = findDirectoryNode(folderPath); + return $folderNode && $folderNode.hasClass("jstree-open"); }, - "Button should hide on sidebar mouse leave", + "Folder to be opened: " + folderPath, 1000 ); + } - expect($collapseBtn.hasClass("show")).toBe(false); - }); - - it("Should have click handler attached", function () { - const $collapseBtn = $("#collapse-folders"); - const events = $._data($collapseBtn[0], "events"); - - expect(events).toBeTruthy(); - expect(events.click).toBeTruthy(); - expect(events.click.length).toBe(1); - }); - - function findTreeNode(fullPath) { - const $treeItems = testWindow.$("#project-files-container li"); - let $result; - - const name = fullPath.split("/").pop(); + /** + * Helper function to find a directory node in the project tree + * @param {string} path - The path of the directory to find + * @returns {jQuery|null} - The jQuery object for the directory node, or null if not found + */ + function findDirectoryNode(path) { + const dirName = path.split("/").pop(); + const $treeItems = $("#project-files-container li"); + let $result = null; $treeItems.each(function () { - const $treeNode = testWindow.$(this); - if ($treeNode.children("a").text().trim() === name) { + const $treeNode = $(this); + if ($treeNode.children("a").text().trim() === dirName) { $result = $treeNode; - return false; // break the loop + return false; // Break the loop } }); + return $result; } - async function openFolder(folderPath) { - const $treeNode = findTreeNode(folderPath); - expect($treeNode).toBeTruthy(); - - if (!$treeNode.hasClass("jstree-open")) { - $treeNode.children("a").children("span").click(); - - await awaitsFor( - function () { - return $treeNode.hasClass("jstree-open"); - }, - `Open folder ${folderPath}`, - 2000 - ); + /** + * Helper function to check if a folder is open in the project tree + * @param {string} folderPath - The path of the folder to check + * @returns {boolean} - True if the folder is open, false otherwise + */ + function isFolderOpen(folderPath) { + const $folderNode = findDirectoryNode(folderPath); + // If the folder node can't be found, it's definitely not open + if (!$folderNode) { + return false; } + return $folderNode.hasClass("jstree-open"); } - function isFolderOpen(folderPath) { - const $treeNode = findTreeNode(folderPath); - return $treeNode && $treeNode.hasClass("jstree-open"); - } + describe("UI", function () { + it("should have a collapse button in the project files header", async function () { + // Check if the collapse button exists + const $collapseBtn = $("#collapse-folders"); + expect($collapseBtn.length).toBe(1); - function getOpenFolders() { - const openFolders = []; - testWindow.$("#project-files-container li.jstree-open").each(function () { - const $node = testWindow.$(this); - const folderName = $node.children("a").text().trim(); - if (folderName) { - openFolders.push(folderName); - } + // Check if the button has the collapse icons + expect($collapseBtn.find(".collapse-icon").length).toBe(2); }); - return openFolders; - } - it("Should collapse all open directories when clicked", async function () { - // First, open some directories - const directoryPath = testPath + "/directory"; + it("should collapse all open folders when the collapse button is clicked", async function () { + // Open some folders first + await openFolder(testProjectPath + "/folder1"); + await openFolder(testProjectPath + "/folder2"); - await openFolder("directory"); + // Verify folders are open + expect(isFolderOpen(testProjectPath + "/folder1")).toBe(true); + expect(isFolderOpen(testProjectPath + "/folder2")).toBe(true); - // Verify the directory is open - expect(isFolderOpen("directory")).toBe(true); + // Click the collapse button + $("#collapse-folders").click(); - // Show the collapse button by hovering over sidebar - const $sidebar = $("#sidebar"); - const $collapseBtn = $("#collapse-folders"); - $sidebar.trigger("mouseenter"); - - await awaitsFor( - function () { - return $collapseBtn.hasClass("show"); - }, - "Button should show", - 1000 - ); - - // Click the collapse button - $collapseBtn.trigger("click"); - - // Wait for directories to close - await awaitsFor( - function () { - return !isFolderOpen("directory"); - }, - "Directory should be closed after clicking collapse button", - 2000 - ); - - // Verify the directory is now closed - expect(isFolderOpen("directory")).toBe(false); - }); - - it("Should collapse multiple open directories when clicked", async function () { - // Open multiple directories if they exist - await openFolder("directory"); - - // Verify directories are open - expect(isFolderOpen("directory")).toBe(true); - - const initialOpenFolders = getOpenFolders(); - expect(initialOpenFolders.length).toBeGreaterThan(0); - - // Show the collapse button and click it - const $sidebar = $("#sidebar"); - const $collapseBtn = $("#collapse-folders"); - $sidebar.trigger("mouseenter"); - - await awaitsFor( - function () { - return $collapseBtn.hasClass("show"); - }, - "Button should show", - 1000 - ); - - $collapseBtn.trigger("click"); - - // Wait for all directories to close - await awaitsFor( - function () { - return getOpenFolders().length === 0; - }, - "All directories should be closed", - 2000 - ); - - // Verify no directories are open - expect(getOpenFolders().length).toBe(0); - }); - - it("Should handle click when no directories are open", function () { - // Ensure no directories are open initially - const openFolders = getOpenFolders(); - expect(openFolders.length).toBe(0); - - // Show the collapse button and click it - const $sidebar = $("#sidebar"); - const $collapseBtn = $("#collapse-folders"); - $sidebar.trigger("mouseenter"); + // Wait for folders to be collapsed + await awaitsFor( + function () { + return ( + !isFolderOpen(testProjectPath + "/folder1") && !isFolderOpen(testProjectPath + "/folder2") + ); + }, + "Folders to be collapsed", + 1000 + ); - // This should not throw an error - expect(function () { - $collapseBtn.trigger("click"); - }).not.toThrow(); + // Verify folders are closed + expect(isFolderOpen(testProjectPath + "/folder1")).toBe(false); + expect(isFolderOpen(testProjectPath + "/folder2")).toBe(false); + }); - // Should still have no open folders - expect(getOpenFolders().length).toBe(0); - }); + it("should collapse nested folders when the collapse button is clicked", async function () { + // Open folders with nested structure + await openFolder(testProjectPath + "/folder1"); + await openFolder(testProjectPath + "/folder1/subfolder1"); + await openFolder(testProjectPath + "/folder2"); + await openFolder(testProjectPath + "/folder2/subfolder2"); - it("Should work with nested directories", async function () { - // Open a parent directory first - await openFolder("directory"); - expect(isFolderOpen("directory")).toBe(true); + // Verify folders are open + expect(isFolderOpen(testProjectPath + "/folder1")).toBe(true); + expect(isFolderOpen(testProjectPath + "/folder1/subfolder1")).toBe(true); + expect(isFolderOpen(testProjectPath + "/folder2")).toBe(true); + expect(isFolderOpen(testProjectPath + "/folder2/subfolder2")).toBe(true); - // If there are subdirectories, try to open one - // Note: This test assumes the test project has nested directories - const $subdirs = testWindow.$("#project-files-container li.jstree-open li.jstree-closed"); - if ($subdirs.length > 0) { - // Open a subdirectory if one exists - $subdirs.first().children("a").children("span").click(); + // Click the collapse button + $("#collapse-folders").click(); + // Wait for all folders to be collapsed await awaitsFor( function () { - return $subdirs.first().hasClass("jstree-open"); + return ( + !isFolderOpen(testProjectPath + "/folder1") && + !isFolderOpen(testProjectPath + "/folder1/subfolder1") && + !isFolderOpen(testProjectPath + "/folder2") && + !isFolderOpen(testProjectPath + "/folder2/subfolder2") + ); }, - "Open subdirectory", - 2000 + "All folders to be collapsed", + 1000 ); - } - const initialOpenCount = getOpenFolders().length; - expect(initialOpenCount).toBeGreaterThan(0); + // Verify all folders are closed + expect(isFolderOpen(testProjectPath + "/folder1")).toBe(false); + expect(isFolderOpen(testProjectPath + "/folder1/subfolder1")).toBe(false); + expect(isFolderOpen(testProjectPath + "/folder2")).toBe(false); + expect(isFolderOpen(testProjectPath + "/folder2/subfolder2")).toBe(false); + }); - // Show the collapse button and click it - const $sidebar = $("#sidebar"); - const $collapseBtn = $("#collapse-folders"); - $sidebar.trigger("mouseenter"); + it("should do nothing when no folders are open", async function () { + // Make sure all folders are closed + $("#collapse-folders").click(); + await awaitsFor( + function () { + return ( + !isFolderOpen(testProjectPath + "/folder1") && !isFolderOpen(testProjectPath + "/folder2") + ); + }, + "Folders to be collapsed", + 1000 + ); - await awaitsFor( - function () { - return $collapseBtn.hasClass("show"); - }, - "Button should show", - 1000 - ); + // Get the current state of the project tree + const initialState = $("#project-files-container").html(); - $collapseBtn.trigger("click"); + // Click the collapse button again + $("#collapse-folders").click(); - // Wait for all directories to close (including nested ones) - await awaitsFor( - function () { - return getOpenFolders().length === 0; - }, - "All nested directories should be closed", - 2000 - ); + // Wait a bit to ensure any potential changes would have happened + await awaits(300); - expect(getOpenFolders().length).toBe(0); + // Verify the project tree hasn't changed + expect($("#project-files-container").html()).toBe(initialState); + }); }); }); });