From 9d615c8e1953951ad7df1a314048b7f6396c5ab3 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 10:47:30 +0800 Subject: [PATCH 01/10] fix: avoid duplicate project roots in multi-root workspaces Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../languageServerApiManager.ts | 16 ++++--- src/views/dependencyDataProvider.ts | 26 +++++++++-- test/index.ts | 11 +++++ test/maven-suite/projectView.test.ts | 18 ++++++++ test/multiple-suite/index.ts | 38 +++++++++++++++ test/multiple-suite/projectView.test.ts | 46 +++++++++++++++++++ test/multiple/multiple-project.code-workspace | 3 ++ test/non-java/package.json | 4 ++ 8 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 test/multiple-suite/index.ts create mode 100644 test/multiple-suite/projectView.test.ts create mode 100644 test/non-java/package.json diff --git a/src/languageServerApi/languageServerApiManager.ts b/src/languageServerApi/languageServerApiManager.ts index 7affb88c..a89fe4ef 100644 --- a/src/languageServerApi/languageServerApiManager.ts +++ b/src/languageServerApi/languageServerApiManager.ts @@ -116,12 +116,16 @@ class LanguageServerApiManager { // Server is sending project data, so it's definitely running. // Mark as running so ready() returns immediately on subsequent calls. this.isServerRunning = true; - // During import, the JDTLS server is blocked by Eclipse workspace - // operations and cannot respond to queries. Instead of triggering - // a refresh (which queries the server), directly add projects to - // the tree view from the notification data. - const projectUris = uris.map(u => u.toString()); - commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, projectUris); + if (this.isServerReady) { + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_REFRESH, /* debounce = */true); + } else { + // During import, the JDTLS server is blocked by Eclipse workspace + // operations and cannot respond to queries. Instead of triggering + // a refresh (which queries the server), directly add projects to + // the tree view from the notification data. + const projectUris = uris.map(u => u.toString()); + commands.executeCommand(Commands.VIEW_PACKAGE_INTERNAL_ADD_PROJECTS, projectUris); + } syncHandler.updateFileWatcher(Settings.autoRefresh()); })); } diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index 626af103..6eb582f4 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import * as _ from "lodash"; +import * as path from "path"; import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, RelativePattern, TreeDataProvider, TreeItem, Uri, window, workspace, @@ -226,7 +227,10 @@ export class DependencyDataProvider implements TreeDataProvider { */ public addProgressiveProjects(projectUris: string[]): void { const folders = workspace.workspaceFolders; - if (!folders || !folders.length) { + // Multi-root workspaces use WorkspaceNode roots, so inserting ProjectNode + // roots would create a mixed and invalid tree structure. Wait for the + // full server-ready refresh instead. + if (!folders || folders.length !== 1) { return; } @@ -238,11 +242,14 @@ export class DependencyDataProvider implements TreeDataProvider { this._rootItems .filter((n): n is ProjectNode => n instanceof ProjectNode) .map((n) => n.uri) + .filter((uri): uri is string => Boolean(uri)) + .map(getProjectUriKey) ); let added = false; for (const uriStr of projectUris) { - if (existingUris.has(uriStr)) { + const uriKey = getProjectUriKey(uriStr); + if (existingUris.has(uriKey)) { continue; } // Extract project name from URI (last non-empty path segment) @@ -253,7 +260,7 @@ export class DependencyDataProvider implements TreeDataProvider { kind: NodeKind.Project, }; this._rootItems.push(new ProjectNode(nodeData, undefined)); - existingUris.add(uriStr); + existingUris.add(uriKey); added = true; } @@ -308,3 +315,16 @@ export class DependencyDataProvider implements TreeDataProvider { } } } + +function getProjectUriKey(uriString: string): string { + const uri = Uri.parse(uriString); + if (uri.scheme !== "file") { + return uri.toString(); + } + + let fsPath = path.normalize(uri.fsPath); + if (fsPath !== path.parse(fsPath).root) { + fsPath = fsPath.replace(/[\\\/]+$/, ""); + } + return process.platform === "win32" ? fsPath.toLowerCase() : fsPath; +} diff --git a/test/index.ts b/test/index.ts index f9db1d22..a08d7358 100644 --- a/test/index.ts +++ b/test/index.ts @@ -98,6 +98,17 @@ async function main(): Promise { ], }); + // Run multi-root workspace test + await runTests({ + vscodeExecutablePath, + extensionDevelopmentPath, + extensionTestsPath: path.resolve(__dirname, "./multiple-suite"), + launchArgs: [ + path.join(__dirname, "..", "..", "test", "multiple", "multiple-project.code-workspace"), + `--user-data-dir=${userDir}`, + ], + }); + // Run test for non-Java Gradle project (regression test for #921) await runTests({ vscodeExecutablePath, diff --git a/test/maven-suite/projectView.test.ts b/test/maven-suite/projectView.test.ts index c3999802..f5c545f4 100644 --- a/test/maven-suite/projectView.test.ts +++ b/test/maven-suite/projectView.test.ts @@ -277,6 +277,24 @@ suite("Maven Project View Tests", () => { assert.equal(mavenChildren[1].getDisplayName(), "junit:junit:4.13.1"); }); + test("Does not add duplicate progressive projects for equivalent URIs", async function() { + const explorer = DependencyExplorer.getInstance(contextManager.context); + await vscode.commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH); + + const roots = await explorer.dataProvider.getChildren(); + assert.equal(roots?.length, 1, "Number of root nodes should be 1"); + const projectNode = roots![0] as ProjectNode; + assert.ok(projectNode.uri, "Project node should have a URI"); + + const equivalentUri = projectNode.uri!.endsWith("/") + ? projectNode.uri!.replace(/\/+$/, "") + : `${projectNode.uri}/`; + explorer.dataProvider.addProgressiveProjects([equivalentUri]); + + const updatedRoots = await explorer.dataProvider.getChildren(); + assert.equal(updatedRoots?.length, 1, "Equivalent project URIs should be deduplicated"); + }); + teardown(async () => { // Restore default settings. Some tests might alter them and others depend on a specific setting. // Not resetting to the default settings will also show the file as changed in the source control view. diff --git a/test/multiple-suite/index.ts b/test/multiple-suite/index.ts new file mode 100644 index 00000000..cfc957e9 --- /dev/null +++ b/test/multiple-suite/index.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as glob from "glob"; +import * as Mocha from "mocha"; +import * as path from "path"; + +export function run(): Promise { + const mocha = new Mocha({ + ui: "tdd", + color: true, + timeout: 1 * 60 * 1000, + }); + + const testsRoot = __dirname; + + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + e(err); + } + }); + }); +} diff --git a/test/multiple-suite/projectView.test.ts b/test/multiple-suite/projectView.test.ts new file mode 100644 index 00000000..6160ab2f --- /dev/null +++ b/test/multiple-suite/projectView.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as assert from "assert"; +import * as vscode from "vscode"; +import { + Commands, contextManager, DependencyExplorer, ProjectNode, WorkspaceNode, +} from "../../extension.bundle"; +import { setupTestEnv } from "../shared"; + +// tslint:disable: only-arrow-functions +suite("Multiple Project View Tests", () => { + + suiteSetup(async () => { + await setupTestEnv(); + const javaExtension = vscode.extensions.getExtension("redhat.java"); + assert.ok(javaExtension, "Language Support for Java should be installed"); + const javaApi = await javaExtension!.activate(); + await javaApi.serverReady(); + await vscode.commands.executeCommand(Commands.VIEW_PACKAGE_REFRESH); + }); + + test("Does not add project roots progressively in a multi-root workspace", async function() { + const explorer = DependencyExplorer.getInstance(contextManager.context); + const roots = await explorer.dataProvider.getChildren(); + const expectedRootCount = vscode.workspace.workspaceFolders?.length || 0; + + assert.equal(roots?.length, expectedRootCount, "Each workspace folder should have one root node"); + assert.ok(roots?.every(root => root instanceof WorkspaceNode), "All roots should be workspace nodes"); + const nonJavaRoot = roots?.find(root => + root instanceof WorkspaceNode && root.name === "non-java") as WorkspaceNode | undefined; + assert.ok(nonJavaRoot, "The non-Java workspace folder should have a root node"); + assert.equal((await nonJavaRoot!.getChildren()).length, 0, "The non-Java root should not contain Java projects"); + + const projects = await explorer.dataProvider.getRootProjects(); + const project = projects.find((node): node is ProjectNode => + node instanceof ProjectNode && Boolean(node.uri)); + assert.ok(project?.uri, "At least one Java project should be available"); + + explorer.dataProvider.addProgressiveProjects([project!.uri!]); + + const updatedRoots = await explorer.dataProvider.getChildren(); + assert.equal(updatedRoots?.length, expectedRootCount, "Progressive updates should not add project roots"); + assert.ok(updatedRoots?.every(root => root instanceof WorkspaceNode), "All roots should remain workspace nodes"); + }); +}); diff --git a/test/multiple/multiple-project.code-workspace b/test/multiple/multiple-project.code-workspace index 19610b08..389a49dc 100644 --- a/test/multiple/multiple-project.code-workspace +++ b/test/multiple/multiple-project.code-workspace @@ -8,6 +8,9 @@ }, { "path": "..\\gradle" + }, + { + "path": "..\\non-java" } ], "settings": {} diff --git a/test/non-java/package.json b/test/non-java/package.json new file mode 100644 index 00000000..030de1b7 --- /dev/null +++ b/test/non-java/package.json @@ -0,0 +1,4 @@ +{ + "name": "non-java", + "private": true +} From 5694074c8f9d024faeea10f1a83fdaf8fafe5ce4 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 11:54:24 +0800 Subject: [PATCH 02/10] test: use portable multi-root workspace paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/multiple/multiple-project.code-workspace | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/multiple/multiple-project.code-workspace b/test/multiple/multiple-project.code-workspace index 389a49dc..6189dcb6 100644 --- a/test/multiple/multiple-project.code-workspace +++ b/test/multiple/multiple-project.code-workspace @@ -1,16 +1,16 @@ { "folders": [ { - "path": "..\\simple" + "path": "../simple" }, { - "path": "..\\maven" + "path": "../maven" }, { - "path": "..\\gradle" + "path": "../gradle" }, { - "path": "..\\non-java" + "path": "../non-java" } ], "settings": {} From a23addd2c348dae50b6701b1cc2144d5440e029f Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 12:24:32 +0800 Subject: [PATCH 03/10] test: cover mixed multi-root project imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/e2e-plans/java-dep-project-explorer.yaml | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test/e2e-plans/java-dep-project-explorer.yaml b/test/e2e-plans/java-dep-project-explorer.yaml index 7e198f6b..cbc1b6c4 100644 --- a/test/e2e-plans/java-dep-project-explorer.yaml +++ b/test/e2e-plans/java-dep-project-explorer.yaml @@ -21,6 +21,7 @@ setup: timeout: 180 settings: java.configuration.checkProjectSettingsExclusions: false + java.dependency.refreshDelay: 120000 workbench.startupEditor: "none" steps: @@ -152,3 +153,78 @@ steps: name: "App" exact: true timeout: 15 + + # ── Test 5: mixed multi-root project attribution (#1060) ── + # First establish a mixed workspace and refresh it into WorkspaceNode roots. + # The smoke-test driver renders folder pickers as an internal quick input: + # entering an absolute folder path opens it, then the Add button confirms it. + - id: "invoke-add-non-java-root" + action: "executeVSCodeCommand workbench.action.addRootFolder" + + - id: "type-non-java-root" + action: "fillQuickInput ${workspaceParent}/non-java" + + - id: "confirm-non-java-root" + action: "tryClickButton Add" + + - id: "wait-non-java-root-ready" + action: "waitForLanguageServer" + timeout: 120 + skipLlmVerify: true + + - id: "refresh-mixed-workspace" + action: "executeVSCodeCommand java.view.package.refresh" + waitBefore: 2 + + - id: "collapse-multi-root-explorer" + action: "collapseSidebarSection UNTITLED (WORKSPACE)" + + - id: "focus-mixed-workspace" + action: "executeVSCodeCommand javaProjectExplorer.focus" + waitBefore: 2 + + - id: "verify-non-java-workspace-root" + action: "wait 1 seconds" + verifyTreeItem: + name: "non-java" + timeout: 15 + + # Add a Java folder after the mixed multi-root structure already exists. + # The long refresh delay keeps the workspace-folder refresh pending while the + # project-import notification arrives. It must not append a top-level + # ProjectNode named "simple" to the existing WorkspaceNode roots. + - id: "invoke-add-java-root" + action: "executeVSCodeCommand workbench.action.addRootFolder" + + - id: "type-java-root" + action: "fillQuickInput ${workspaceParent}/simple" + + - id: "confirm-java-root" + action: "tryClickButton Add" + + - id: "wait-java-root-import" + action: "wait 5 seconds" + + - id: "wait-java-root-ready" + action: "waitForLanguageServer" + timeout: 120 + skipLlmVerify: true + + - id: "focus-before-structural-refresh" + action: "executeVSCodeCommand javaProjectExplorer.focus" + + - id: "verify-no-progressive-project-root" + action: "wait 1 seconds" + verifyTreeItem: + name: "simple" + visible: false + timeout: 5 + + - id: "refresh-added-java-root" + action: "executeVSCodeCommand java.view.package.refresh" + + - id: "verify-java-workspace-root" + action: "wait 2 seconds" + verifyTreeItem: + name: "simple" + timeout: 15 From d28cb8a7db5f86b8922ae721d01cae655a58d6a5 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 13:13:03 +0800 Subject: [PATCH 04/10] test: make multi-root E2E deterministic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/e2eUI.yml | 12 +++--- test/e2e-plans/java-dep-project-explorer.yaml | 38 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/e2eUI.yml b/.github/workflows/e2eUI.yml index 96855ee7..f5f0dfd4 100644 --- a/.github/workflows/e2eUI.yml +++ b/.github/workflows/e2eUI.yml @@ -181,10 +181,10 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Setup autotest - run: npm install -g @vscjava/vscode-autotest + run: npm install -g git+https://github.com/wenytang-ms/javaext-autotest.git#e0375659a4cfbe669f42adca4272e80dadff9388 - name: Download VSIX artifact uses: actions/download-artifact@v4 @@ -234,10 +234,10 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Setup autotest - run: npm install -g @vscjava/vscode-autotest + run: npm install -g git+https://github.com/wenytang-ms/javaext-autotest.git#e0375659a4cfbe669f42adca4272e80dadff9388 - name: Download VSIX artifact uses: actions/download-artifact@v4 @@ -272,10 +272,10 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Setup autotest - run: npm install -g @vscjava/vscode-autotest + run: npm install -g git+https://github.com/wenytang-ms/javaext-autotest.git#e0375659a4cfbe669f42adca4272e80dadff9388 - name: Download all plan results uses: actions/download-artifact@v4 diff --git a/test/e2e-plans/java-dep-project-explorer.yaml b/test/e2e-plans/java-dep-project-explorer.yaml index cbc1b6c4..73854cf6 100644 --- a/test/e2e-plans/java-dep-project-explorer.yaml +++ b/test/e2e-plans/java-dep-project-explorer.yaml @@ -21,7 +21,6 @@ setup: timeout: 180 settings: java.configuration.checkProjectSettingsExclusions: false - java.dependency.refreshDelay: 120000 workbench.startupEditor: "none" steps: @@ -189,10 +188,8 @@ steps: name: "non-java" timeout: 15 - # Add a Java folder after the mixed multi-root structure already exists. - # The long refresh delay keeps the workspace-folder refresh pending while the - # project-import notification arrives. It must not append a top-level - # ProjectNode named "simple" to the existing WorkspaceNode roots. + # Add a Java folder after the mixed multi-root structure already exists and + # refresh it into the expected WorkspaceNode -> ProjectNode hierarchy. - id: "invoke-add-java-root" action: "executeVSCodeCommand workbench.action.addRootFolder" @@ -202,29 +199,32 @@ steps: - id: "confirm-java-root" action: "tryClickButton Add" - - id: "wait-java-root-import" - action: "wait 5 seconds" - - id: "wait-java-root-ready" action: "waitForLanguageServer" timeout: 120 skipLlmVerify: true - - id: "focus-before-structural-refresh" - action: "executeVSCodeCommand javaProjectExplorer.focus" - - - id: "verify-no-progressive-project-root" - action: "wait 1 seconds" - verifyTreeItem: - name: "simple" - visible: false - timeout: 5 - - id: "refresh-added-java-root" action: "executeVSCodeCommand java.view.package.refresh" - id: "verify-java-workspace-root" - action: "wait 2 seconds" + action: "executeVSCodeCommand javaProjectExplorer.focus" verifyTreeItem: name: "simple" + exact: true + count: 1 + level: 1 + timeout: 15 + + # Deterministically simulate the onDidProjectsImport path. Before #1060 this + # command appended a second top-level ProjectNode named "simple" beside the + # existing WorkspaceNode. The fixed provider ignores progressive insertion + # in multi-root workspaces, so exactly one level-1 row remains. + - id: "simulate-progressive-project-import" + action: 'executeVSCodeCommand _java.view.package.internal.addProjects ["${workspaceParentUri}/simple"]' + verifyTreeItem: + name: "simple" + exact: true + count: 1 + level: 1 timeout: 15 From 17dffa634a7b86f05321a12effa315d02fffe306 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 13:19:09 +0800 Subject: [PATCH 05/10] ci: use released AutoTest tree verifier Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/e2eUI.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2eUI.yml b/.github/workflows/e2eUI.yml index f5f0dfd4..bff98bed 100644 --- a/.github/workflows/e2eUI.yml +++ b/.github/workflows/e2eUI.yml @@ -184,7 +184,7 @@ jobs: node-version: 22 - name: Setup autotest - run: npm install -g git+https://github.com/wenytang-ms/javaext-autotest.git#e0375659a4cfbe669f42adca4272e80dadff9388 + run: npm install -g @vscjava/vscode-autotest@0.7.25 - name: Download VSIX artifact uses: actions/download-artifact@v4 @@ -237,7 +237,7 @@ jobs: node-version: 22 - name: Setup autotest - run: npm install -g git+https://github.com/wenytang-ms/javaext-autotest.git#e0375659a4cfbe669f42adca4272e80dadff9388 + run: npm install -g @vscjava/vscode-autotest@0.7.25 - name: Download VSIX artifact uses: actions/download-artifact@v4 @@ -275,7 +275,7 @@ jobs: node-version: 22 - name: Setup autotest - run: npm install -g git+https://github.com/wenytang-ms/javaext-autotest.git#e0375659a4cfbe669f42adca4272e80dadff9388 + run: npm install -g @vscjava/vscode-autotest@0.7.25 - name: Download all plan results uses: actions/download-artifact@v4 From 4d3a5718c28f0b13e8381951481988a15bf3dfb0 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 13:21:45 +0800 Subject: [PATCH 06/10] ci: consume latest AutoTest release Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/e2eUI.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2eUI.yml b/.github/workflows/e2eUI.yml index bff98bed..faad5a5a 100644 --- a/.github/workflows/e2eUI.yml +++ b/.github/workflows/e2eUI.yml @@ -184,7 +184,7 @@ jobs: node-version: 22 - name: Setup autotest - run: npm install -g @vscjava/vscode-autotest@0.7.25 + run: npm install -g @vscjava/vscode-autotest - name: Download VSIX artifact uses: actions/download-artifact@v4 @@ -237,7 +237,7 @@ jobs: node-version: 22 - name: Setup autotest - run: npm install -g @vscjava/vscode-autotest@0.7.25 + run: npm install -g @vscjava/vscode-autotest - name: Download VSIX artifact uses: actions/download-artifact@v4 @@ -275,7 +275,7 @@ jobs: node-version: 22 - name: Setup autotest - run: npm install -g @vscjava/vscode-autotest@0.7.25 + run: npm install -g @vscjava/vscode-autotest - name: Download all plan results uses: actions/download-artifact@v4 From a08b224ca0f9ebd73d2c3a7a3049e954b715ed3f Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 13:51:21 +0800 Subject: [PATCH 07/10] fix: preserve cached workspace roots during transitions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/views/dependencyDataProvider.ts | 8 +++---- test/multiple-suite/projectView.test.ts | 31 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index 6eb582f4..a8b192d6 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -227,10 +227,10 @@ export class DependencyDataProvider implements TreeDataProvider { */ public addProgressiveProjects(projectUris: string[]): void { const folders = workspace.workspaceFolders; - // Multi-root workspaces use WorkspaceNode roots, so inserting ProjectNode - // roots would create a mixed and invalid tree structure. Wait for the - // full server-ready refresh instead. - if (!folders || folders.length !== 1) { + // Multi-root workspaces use WorkspaceNode roots. Those roots can remain + // cached briefly after switching to a single folder, so wait for the + // full refresh rather than creating a mixed root structure. + if (!folders || folders.length !== 1 || this._rootItems?.some(root => root instanceof WorkspaceNode)) { return; } diff --git a/test/multiple-suite/projectView.test.ts b/test/multiple-suite/projectView.test.ts index 6160ab2f..169356ad 100644 --- a/test/multiple-suite/projectView.test.ts +++ b/test/multiple-suite/projectView.test.ts @@ -43,4 +43,35 @@ suite("Multiple Project View Tests", () => { assert.equal(updatedRoots?.length, expectedRootCount, "Progressive updates should not add project roots"); assert.ok(updatedRoots?.every(root => root instanceof WorkspaceNode), "All roots should remain workspace nodes"); }); + + test("Does not add project roots while cached multi-root roots are stale", async function() { + const explorer = DependencyExplorer.getInstance(contextManager.context); + const roots = await explorer.dataProvider.getChildren(); + const folders = vscode.workspace.workspaceFolders; + assert.ok(folders && folders.length > 1, "The test requires a multi-root workspace"); + assert.ok(roots?.every(root => root instanceof WorkspaceNode), "All cached roots should be workspace nodes"); + + const projects = await explorer.dataProvider.getRootProjects(); + const project = projects.find((node): node is ProjectNode => + node instanceof ProjectNode && Boolean(node.uri)); + assert.ok(project?.uri, "At least one Java project should be available"); + + const removedFolders = folders!.slice(1); + assert.ok(vscode.workspace.updateWorkspaceFolders(1, removedFolders.length), + "The workspace should switch to a single folder"); + + try { + assert.equal(vscode.workspace.workspaceFolders?.length, 1, "The workspace should have one folder"); + explorer.dataProvider.addProgressiveProjects([project!.uri!]); + + const updatedRoots = await explorer.dataProvider.getChildren(); + assert.equal(updatedRoots?.length, roots?.length, "Stale cached roots should not be mixed with project roots"); + assert.ok(updatedRoots?.every(root => root instanceof WorkspaceNode), + "Cached workspace roots should remain unchanged until refresh"); + } finally { + assert.ok(vscode.workspace.updateWorkspaceFolders(1, 0, + ...removedFolders.map(folder => ({ uri: folder.uri, name: folder.name }))), + "The removed workspace folders should be restored"); + } + }); }); From 5da037b7440c1074a6a41bb6f4db0b6406105aab Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 19 Aug 2026 15:08:46 +0800 Subject: [PATCH 08/10] test: wait for workspace folder changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 15fc06b3-db1a-46df-9a34-7e8cd1128ba4 --- test/multiple-suite/projectView.test.ts | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/test/multiple-suite/projectView.test.ts b/test/multiple-suite/projectView.test.ts index 169356ad..d744e4df 100644 --- a/test/multiple-suite/projectView.test.ts +++ b/test/multiple-suite/projectView.test.ts @@ -57,7 +57,7 @@ suite("Multiple Project View Tests", () => { assert.ok(project?.uri, "At least one Java project should be available"); const removedFolders = folders!.slice(1); - assert.ok(vscode.workspace.updateWorkspaceFolders(1, removedFolders.length), + const workspaceFoldersChanged = updateWorkspaceFoldersAndWait(1, removedFolders.length, [], "The workspace should switch to a single folder"); try { @@ -69,9 +69,31 @@ suite("Multiple Project View Tests", () => { assert.ok(updatedRoots?.every(root => root instanceof WorkspaceNode), "Cached workspace roots should remain unchanged until refresh"); } finally { - assert.ok(vscode.workspace.updateWorkspaceFolders(1, 0, - ...removedFolders.map(folder => ({ uri: folder.uri, name: folder.name }))), + await workspaceFoldersChanged; + await updateWorkspaceFoldersAndWait(1, 0, + removedFolders.map(folder => ({ uri: folder.uri })), "The removed workspace folders should be restored"); } }); }); + +async function updateWorkspaceFoldersAndWait( + start: number, + deleteCount: number, + foldersToAdd: Array<{ uri: vscode.Uri; name?: string }>, + failureMessage: string, +): Promise { + let resolveChange: () => void; + const changed = new Promise((resolve) => resolveChange = resolve); + const listener = vscode.workspace.onDidChangeWorkspaceFolders(() => { + listener.dispose(); + resolveChange(); + }); + + if (!vscode.workspace.updateWorkspaceFolders(start, deleteCount, ...foldersToAdd)) { + listener.dispose(); + assert.fail(failureMessage); + } + + await changed; +} From 841cbd750b8f42d0034e178f37f9ad95dd991bac Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 19 Aug 2026 15:23:06 +0800 Subject: [PATCH 09/10] style: use preferred array type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 15fc06b3-db1a-46df-9a34-7e8cd1128ba4 --- test/multiple-suite/projectView.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/multiple-suite/projectView.test.ts b/test/multiple-suite/projectView.test.ts index d744e4df..2e10c3db 100644 --- a/test/multiple-suite/projectView.test.ts +++ b/test/multiple-suite/projectView.test.ts @@ -80,7 +80,7 @@ suite("Multiple Project View Tests", () => { async function updateWorkspaceFoldersAndWait( start: number, deleteCount: number, - foldersToAdd: Array<{ uri: vscode.Uri; name?: string }>, + foldersToAdd: { uri: vscode.Uri; name?: string }[], failureMessage: string, ): Promise { let resolveChange: () => void; From efd3711a648cc3317d528d04cb3e83dd74c411cd Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Wed, 19 Aug 2026 16:50:15 +0800 Subject: [PATCH 10/10] ci: use preinstalled xvfb-run for UI tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/e2eUI.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/e2eUI.yml b/.github/workflows/e2eUI.yml index faad5a5a..89264f24 100644 --- a/.github/workflows/e2eUI.yml +++ b/.github/workflows/e2eUI.yml @@ -162,16 +162,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Build Environment (Xvfb) - run: | - sudo apt-get update - sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 - # Use 1920x1080 so the Java Projects view (rendered inside the Explorer - # sidebar) gets enough vertical space. With 1024x768 the sticky - # pane-header overlapped tree rows and intercepted click events. - sudo /usr/bin/Xvfb :99 -screen 0 1920x1080x24 > /dev/null 2>&1 & - sleep 3 - - name: Set up JDK 21 uses: actions/setup-java@v4 with: @@ -199,7 +189,9 @@ jobs: AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }} run: | - DISPLAY=:99 autotest run "test/e2e-plans/${{ matrix.plan }}.yaml" \ + # Use 1920x1080 so the Java Projects view gets enough vertical space. + xvfb-run -a -s "-screen 0 1920x1080x24" \ + autotest run "test/e2e-plans/${{ matrix.plan }}.yaml" \ --vsix "$(pwd)/vscode-java-dependency.vsix" \ --output "test-results/${{ matrix.plan }}"