diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json
index 69607dfbbd..b75f6eb896 100644
--- a/localization/l10n/bundle.l10n.json
+++ b/localization/l10n/bundle.l10n.json
@@ -747,6 +747,7 @@
]
},
"Enable Experiences & Reload": "Enable Experiences & Reload",
+ "Error loading; refresh to try again": "Error loading; refresh to try again",
"Connection Dialog (Preview)": "Connection Dialog (Preview)",
"Azure Account": "Azure Account",
"Azure Account is required": "Azure Account is required",
diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf
index 5a42eca8fc..5e8e668e03 100644
--- a/localization/xliff/vscode-mssql.xlf
+++ b/localization/xliff/vscode-mssql.xlf
@@ -506,6 +506,9 @@
Error loading preview
+
+ Error loading; refresh to try again
+
Error occurred opening content in editor.
diff --git a/src/constants/locConstants.ts b/src/constants/locConstants.ts
index affd48b330..7adc847946 100644
--- a/src/constants/locConstants.ts
+++ b/src/constants/locConstants.ts
@@ -680,6 +680,12 @@ export function enableRichExperiencesPrompt(learnMoreUrl: string) {
}
export let enableRichExperiences = l10n.t("Enable Experiences & Reload");
+export class ObjectExplorer {
+ public static ErrorLoadingRefreshToTryAgain = l10n.t(
+ "Error loading; refresh to try again",
+ );
+}
+
export class ConnectionDialog {
public static connectionDialog = l10n.t("Connection Dialog (Preview)");
public static azureAccount = l10n.t("Azure Account");
diff --git a/src/objectExplorer/objectExplorerService.ts b/src/objectExplorer/objectExplorerService.ts
index df1d4c80bd..4e8fdd4b5d 100644
--- a/src/objectExplorer/objectExplorerService.ts
+++ b/src/objectExplorer/objectExplorerService.ts
@@ -357,10 +357,19 @@ export class ObjectExplorerService {
return undefined;
}
- private handleExpandSessionNotification(): NotificationHandler {
+ /**
+ * Handler for async response from SQL Tools Service.
+ * Public only for testing
+ */
+ public handleExpandSessionNotification(): NotificationHandler {
const self = this;
const handler = (result: ExpandResponse) => {
- if (result && result.nodes) {
+ if (!result) {
+ return undefined;
+ }
+
+ if (result.nodes && !result.errorMessage) {
+ // successfully received children from SQL Tools Service
const credentials =
self._sessionIdToConnectionCredentialsMap.get(
result.sessionId,
@@ -402,6 +411,42 @@ export class ObjectExplorerService {
return;
}
}
+ } else {
+ // failure to expand node; display error
+
+ if (result.errorMessage) {
+ self._connectionManager.vscodeWrapper.showErrorMessage(
+ result.errorMessage,
+ );
+ }
+
+ const expandParams: ExpandParams = {
+ sessionId: result.sessionId,
+ nodePath: result.nodePath,
+ };
+ const parentNode = self.getParentFromExpandParams(expandParams);
+
+ const errorNode = new vscode.TreeItem(
+ LocalizedConstants.ObjectExplorer.ErrorLoadingRefreshToTryAgain,
+ TreeItemCollapsibleState.None,
+ );
+
+ errorNode.tooltip = result.errorMessage;
+
+ self._treeNodeToChildrenMap.set(parentNode, [errorNode]);
+
+ for (let key of self._expandParamsToPromiseMap.keys()) {
+ if (
+ key.sessionId === expandParams.sessionId &&
+ key.nodePath === expandParams.nodePath
+ ) {
+ let promise = self._expandParamsToPromiseMap.get(key);
+ promise.resolve([errorNode as TreeNodeInfo]);
+ self._expandParamsToPromiseMap.delete(key);
+ self._expandParamsToTreeNodeInfoMap.delete(key);
+ return;
+ }
+ }
}
};
return handler;
diff --git a/test/unit/objectExplorerProvider.test.ts b/test/unit/objectExplorerProvider.test.ts
index 6b6a020ca0..d53e8ed490 100644
--- a/test/unit/objectExplorerProvider.test.ts
+++ b/test/unit/objectExplorerProvider.test.ts
@@ -18,12 +18,18 @@ import { AccountSignInTreeNode } from "../../src/objectExplorer/accountSignInTre
import { ConnectTreeNode } from "../../src/objectExplorer/connectTreeNode";
import { NodeInfo } from "../../src/models/contracts/objectExplorer/nodeInfo";
import { Deferred } from "../../src/protocol";
+import {
+ ExpandParams,
+ ExpandResponse,
+} from "../../src/models/contracts/objectExplorer/expandNodeRequest";
+import VscodeWrapper from "../../src/controllers/vscodeWrapper";
-suite("Object Explorer Provider Tests", () => {
+suite("Object Explorer Provider Tests", function () {
let objectExplorerService: TypeMoq.IMock;
let connectionManager: TypeMoq.IMock;
let client: TypeMoq.IMock;
let objectExplorerProvider: ObjectExplorerProvider;
+ let vscodeWrapper: TypeMoq.IMock;
setup(() => {
let mockContext: TypeMoq.IMock =
@@ -42,6 +48,21 @@ suite("Object Explorer Provider Tests", () => {
c.onNotification(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
);
connectionManager.object.client = client.object;
+
+ vscodeWrapper = TypeMoq.Mock.ofType(
+ VscodeWrapper,
+ TypeMoq.MockBehavior.Loose,
+ );
+
+ vscodeWrapper.setup((v) =>
+ v.showErrorMessage(TypeMoq.It.isAnyString()),
+ );
+
+ connectionManager
+ .setup((c) => c.vscodeWrapper)
+ .returns(() => vscodeWrapper.object);
+ connectionManager.object.vscodeWrapper = vscodeWrapper.object;
+
objectExplorerProvider = new ObjectExplorerProvider(
connectionManager.object,
);
@@ -49,6 +70,7 @@ suite("Object Explorer Provider Tests", () => {
objectExplorerProvider,
"Object Explorer Provider is initialzied properly",
).is.not.equal(undefined);
+
objectExplorerService = TypeMoq.Mock.ofType(
ObjectExplorerService,
TypeMoq.MockBehavior.Loose,
@@ -334,6 +356,146 @@ suite("Object Explorer Provider Tests", () => {
assert.equal(treeItem, node);
});
+ const mockParentTreeNode = new TreeNodeInfo(
+ "Parent Node",
+ undefined,
+ undefined,
+ "parentNodePath",
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ );
+
+ test("Test handleExpandSessionNotification returns child nodes upon success", async function () {
+ const childNodeInfo: NodeInfo = {
+ nodePath: `${mockParentTreeNode.nodePath}/childNodePath`,
+ nodeStatus: undefined,
+ nodeSubType: undefined,
+ nodeType: undefined,
+ label: "Child Node",
+ isLeaf: true,
+ errorMessage: undefined,
+ metadata: undefined,
+ };
+
+ const mockExpandResponse: ExpandResponse = {
+ sessionId: "test_session",
+ nodePath: mockParentTreeNode.nodePath,
+ nodes: [childNodeInfo],
+ errorMessage: undefined,
+ };
+
+ const testOeService = new ObjectExplorerService(
+ connectionManager.object,
+ objectExplorerProvider,
+ );
+
+ let notificationObject =
+ testOeService.handleExpandSessionNotification();
+
+ const expandParams: ExpandParams = {
+ sessionId: mockExpandResponse.sessionId,
+ nodePath: mockExpandResponse.nodePath,
+ };
+
+ testOeService["_expandParamsToTreeNodeInfoMap"].set(
+ expandParams,
+ mockParentTreeNode,
+ );
+
+ testOeService["_sessionIdToConnectionCredentialsMap"].set(
+ mockExpandResponse.sessionId,
+ undefined,
+ );
+
+ const outputPromise = new Deferred();
+
+ testOeService["_expandParamsToPromiseMap"].set(
+ expandParams,
+ outputPromise,
+ );
+
+ notificationObject.call(testOeService, mockExpandResponse);
+
+ const childNodes = await outputPromise;
+ assert.equal(childNodes.length, 1, "Child nodes length");
+ assert.equal(
+ childNodes[0].label,
+ childNodeInfo.label,
+ "Child node label",
+ );
+ assert.equal(
+ childNodes[0].nodePath,
+ childNodeInfo.nodePath,
+ "Child node path",
+ );
+ });
+
+ test("Test handleExpandSessionNotification returns message node upon failure", async function () {
+ this.timeout(0);
+
+ const mockExpandResponse: ExpandResponse = {
+ sessionId: "test_session",
+ nodePath: mockParentTreeNode.nodePath,
+ nodes: [],
+ errorMessage: "Error occurred when expanding node",
+ };
+
+ const testOeService = new ObjectExplorerService(
+ connectionManager.object,
+ objectExplorerProvider,
+ );
+
+ let notificationObject =
+ testOeService.handleExpandSessionNotification();
+
+ const expandParams: ExpandParams = {
+ sessionId: mockExpandResponse.sessionId,
+ nodePath: mockExpandResponse.nodePath,
+ };
+
+ testOeService["_expandParamsToTreeNodeInfoMap"].set(
+ expandParams,
+ mockParentTreeNode,
+ );
+
+ testOeService["_sessionIdToConnectionCredentialsMap"].set(
+ mockExpandResponse.sessionId,
+ undefined,
+ );
+
+ const outputPromise = new Deferred();
+
+ testOeService["_expandParamsToPromiseMap"].set(
+ expandParams,
+ outputPromise,
+ );
+
+ notificationObject.call(testOeService, mockExpandResponse);
+
+ const childNodes = await outputPromise;
+
+ vscodeWrapper.verify(
+ (x) => x.showErrorMessage(mockExpandResponse.errorMessage),
+ TypeMoq.Times.once(),
+ );
+
+ assert.equal(childNodes.length, 1, "Child nodes length");
+ assert.equal(
+ childNodes[0].label,
+ "Error loading; refresh to try again",
+ "Error node label",
+ );
+ assert.equal(
+ childNodes[0].tooltip,
+ mockExpandResponse.errorMessage,
+ "Error node tooltip",
+ );
+ });
+
test("Test signInNode function", () => {
objectExplorerService.setup((s) =>
s.signInNodeServer(TypeMoq.It.isAny()),