From 82648fab3ea8f16850b5d90feafa6e956fd04838 Mon Sep 17 00:00:00 2001 From: Alex Ma Date: Wed, 30 Sep 2020 08:39:54 -0700 Subject: [PATCH] connection tests for connectionDialogWidget (#11160) * Added providerRegistered test * test for EditConnectionDialog * changed wording * added test for connectionInfo * utils.ts tests added * hasRegisteredServers test * commented out editconnection tests, addl. tests * added onConnectionChangedNotification test * added changeGroupId tests * Added connection profile changes * added connectIfNotConnected test * added delete connection test * isRecent and disconnect editor tests * Add CodeQL Analysis workflow (#10195) * Add CodeQL Analysis workflow * Fix path * added registerIconProvider test * Fix for ensureDefaultLanguageFlavor test * added a few tests * utils prefix test updated * added utils tests * disconnect tests added * Added additional get connection info tests * added some more tests * minor additions to tests * again another commit * another change * connectionManagementService finalized * connectionDialogWidget test WIP * wip connectiondialogwidget test * added working connectionDialogWidget test * added more tests * current connectionDialogWidget tests * undid space * hanging promise addressed * added open test * finished connectionDialogWidget tests * Added showDialog test * mockConnectionDialogService added * added accessorConnectionDialogService * removed accessor service * added openDialogAndWait test * added fake error to test * added error tests * Added comments to test * more coverage * async to await change * registerCapabilities test added * connectionDialogService tests finished * undefined added * Added views for tests * tslint disable added * error catchers added * added empty connectioninfo Co-authored-by: Justin Hutchings --- .../test/common/testConnectionProvider.ts | 6 +- .../browser/connectionDialogWidget.ts | 4 + .../browser/connectionDialogService.test.ts | 301 ++++- .../browser/connectionDialogWidget.test.ts | 199 ++++ .../connectionManagementService.test.ts | 781 ++++++++---- .../connection/test/browser/media/views.css | 170 +++ .../browser/testConnectionDialogWidget.ts | 44 + .../connection/test/browser/testTreeView.ts | 1049 +++++++++++++++++ 8 files changed, 2300 insertions(+), 254 deletions(-) create mode 100644 src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts create mode 100644 src/sql/workbench/services/connection/test/browser/media/views.css create mode 100644 src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts create mode 100644 src/sql/workbench/services/connection/test/browser/testTreeView.ts diff --git a/src/sql/platform/connection/test/common/testConnectionProvider.ts b/src/sql/platform/connection/test/common/testConnectionProvider.ts index b7f1a603c3..782f735852 100644 --- a/src/sql/platform/connection/test/common/testConnectionProvider.ts +++ b/src/sql/platform/connection/test/common/testConnectionProvider.ts @@ -29,10 +29,14 @@ export class TestConnectionProvider implements azdata.ConnectionProvider { return Promise.resolve(true); } - getConnectionString(connectionUri: string): Thenable { + getConnectionString(connectionUri: string, includePassword?: boolean): Thenable { return Promise.resolve(''); } + buildConnectionInfo(connectionString: string): Thenable { + return Promise.resolve({ options: {} }); + } + rebuildIntelliSenseCache(connectionUri: string): Thenable { return Promise.resolve(); } diff --git a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts index a46b91e210..5dc1d13541 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogWidget.ts @@ -156,6 +156,10 @@ export class ConnectionDialogWidget extends Modal implements IViewPaneContainer this.refresh(); } + public getDisplayNameFromProviderName(providerName: string): string { + return this.providerNameToDisplayNameMap[providerName]; + } + public refresh(): void { let filteredProviderMap = this.providerNameToDisplayNameMap; if (this._newConnectionParams && this._newConnectionParams.providers) { diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts index b6bd2e16dd..de2cf5ffcd 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts @@ -3,58 +3,220 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ConnectionDialogService } from 'sql/workbench/services/connection/browser/connectionDialogService'; -import { ConnectionDialogWidget } from 'sql/workbench/services/connection/browser/connectionDialogWidget'; import { ConnectionManagementService } from 'sql/workbench/services/connection/browser/connectionManagementService'; -import { ConnectionType, IConnectableInput, IConnectionResult, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement'; +import { ConnectionType, IConnectableInput, IConnectionResult, INewConnectionParams, IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService'; import * as TypeMoq from 'typemoq'; +import * as assert from 'assert'; +import * as DOM from 'vs/base/browser/dom'; +import * as Constants from 'sql/platform/connection/common/constants'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { NullLogService } from 'vs/platform/log/common/log'; +import { NullLogService, ILogService } from 'vs/platform/log/common/log'; import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; +import { createConnectionProfile } from 'sql/workbench/services/connection/test/browser/connectionManagementService.test'; +import { getUniqueConnectionProvidersByNameMap } from 'sql/workbench/services/connection/test/browser/connectionDialogWidget.test'; +import { TestConnectionDialogWidget } from 'sql/workbench/services/connection/test/browser/testConnectionDialogWidget'; +import { ConnectionDialogService } from 'sql/workbench/services/connection/browser/connectionDialogService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { TestLayoutService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService'; +import { ServiceOptionType, ConnectionOptionSpecialType, IConnectionProfile } from 'sql/platform/connection/common/interfaces'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { ConnectionWidget } from 'sql/workbench/services/connection/browser/connectionWidget'; +import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService'; +import { NullCommandService } from 'vs/platform/commands/common/commands'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { ClearRecentConnectionsAction } from 'sql/workbench/services/connection/browser/connectionActions'; +import { RecentConnectionActionsProvider } from 'sql/workbench/services/connection/browser/recentConnectionTreeController'; +import { RecentConnectionDataSource } from 'sql/workbench/services/objectExplorer/browser/recentConnectionDataSource'; +import { ServerTreeRenderer } from 'sql/workbench/services/objectExplorer/browser/serverTreeRenderer'; +import { RecentConnectionsDragAndDrop } from 'sql/workbench/services/objectExplorer/browser/dragAndDropController'; +import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { Deferred } from 'sql/base/common/promise'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { localize } from 'vs/nls'; +import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewDescriptorService'; +import { ViewContainer, Extensions, IViewsRegistry, IViewContainersRegistry, ITreeViewDescriptor, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { TestTreeView } from 'sql/workbench/services/connection/test/browser/testTreeView'; suite('ConnectionDialogService tests', () => { + const testTreeViewId = 'testTreeView'; + const ViewsRegistry = Registry.as(Extensions.ViewsRegistry); let connectionDialogService: ConnectionDialogService; let mockConnectionManagementService: TypeMoq.Mock; - let mockConnectionDialog: TypeMoq.Mock; + let testConnectionDialog: TestConnectionDialogWidget; + let mockInstantationService: TypeMoq.Mock; + let testConnectionParams: INewConnectionParams; + let connectionProfile: ConnectionProfile; + let mockWidget: TypeMoq.Mock; + let testInstantiationService: TestInstantiationService; + let container: ViewContainer; setup(() => { - let testinstantiationService = new TestInstantiationService(); - testinstantiationService.stub(IStorageService, new TestStorageService()); + const viewInstantiationService: TestInstantiationService = workbenchInstantiationService(); + const viewDescriptorService = viewInstantiationService.createInstance(ViewDescriptorService); + container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + viewInstantiationService.stub(IViewDescriptorService, viewDescriptorService); + const viewDescriptor: ITreeViewDescriptor = { + id: testTreeViewId, + ctorDescriptor: null!, + name: 'Test View 1', + treeView: viewInstantiationService.createInstance(TestTreeView, 'testTree', 'Test Title'), + }; + ViewsRegistry.registerViews([viewDescriptor], container); + + mockInstantationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict); + testInstantiationService = new TestInstantiationService(); + testInstantiationService.stub(IStorageService, new TestStorageService()); + testInstantiationService.stub(ILogService, new NullLogService()); + testInstantiationService.stub(IConfigurationService, new TestConfigurationService()); + testInstantiationService.stub(IInstantiationService, mockInstantationService.object); + testInstantiationService.stub(IViewDescriptorService, viewDescriptorService); let errorMessageService = getMockErrorMessageService(); - connectionDialogService = new ConnectionDialogService(undefined, undefined, errorMessageService.object, - undefined, undefined, undefined, new NullLogService()); + let capabilitiesService = new TestCapabilitiesService(); mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Strict, undefined, // connection store undefined, // connection status manager undefined, // connection dialog service - testinstantiationService, // instantiation service + testInstantiationService, // instantiation service undefined, // editor service undefined, // telemetry service undefined, // configuration service new TestCapabilitiesService()); + testInstantiationService.stub(IConnectionManagementService, mockConnectionManagementService.object); + testInstantiationService.stub(IContextKeyService, new MockContextKeyService()); + testInstantiationService.stub(IThemeService, new TestThemeService()); + testInstantiationService.stub(ILayoutService, new TestLayoutService()); + testInstantiationService.stub(IAdsTelemetryService, new NullAdsTelemetryService()); + connectionDialogService = new ConnectionDialogService(testInstantiationService, capabilitiesService, errorMessageService.object, + new TestConfigurationService(), new BrowserClipboardService(), NullCommandService, new NullLogService()); (connectionDialogService as any)._connectionManagementService = mockConnectionManagementService.object; - mockConnectionDialog = TypeMoq.Mock.ofType(ConnectionDialogWidget, TypeMoq.MockBehavior.Strict, - undefined, // providerDisplayNameOptions - undefined, // selectedProviderType - undefined, // providerNameToDisplayNameMap - undefined, // instantiationService - undefined, // connectionManagementService - undefined, // contextMenuService - undefined, // contextViewService - { getViewContainerById: () => ({}), getViewContainerModel: () => ({}) }, // viewDescriptorService - undefined, // themeService - undefined, // layoutService - undefined, // telemetryService - new MockContextKeyService() - ); - mockConnectionDialog.setup(c => c.resetConnection()); - (connectionDialogService as any)._connectionDialog = mockConnectionDialog.object; + let providerDisplayNames = ['Mock SQL Server']; + let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' }; + mockConnectionManagementService.setup(x => x.getUniqueConnectionProvidersByNameMap(TypeMoq.It.isAny())).returns(() => { + return getUniqueConnectionProvidersByNameMap(providerNameToDisplayMap); + }); + mockConnectionManagementService.setup(x => x.getConnectionGroups(TypeMoq.It.isAny())).returns(() => { + return [new ConnectionProfileGroup('test_group', undefined, 'test_group')]; + }); + testConnectionDialog = new TestConnectionDialogWidget(providerDisplayNames, providerNameToDisplayMap['MSSQL'], providerNameToDisplayMap, testInstantiationService, mockConnectionManagementService.object, undefined, undefined, viewDescriptorService, new TestThemeService(), new TestLayoutService(), new NullAdsTelemetryService(), new MockContextKeyService(), undefined, new NullLogService(), new TestTextResourcePropertiesService(new TestConfigurationService), new TestConfigurationService()); + testConnectionDialog.render(); + testConnectionDialog.renderBody(DOM.createStyleSheet()); + (connectionDialogService as any)._connectionDialog = testConnectionDialog; + + capabilitiesService.capabilities[Constants.mssqlProviderName] = { + connection: { + providerId: Constants.mssqlProviderName, + displayName: 'MSSQL', + connectionOptions: [ + { + name: 'authenticationType', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: [ + { + name: 'authenticationType', + displayName: 'authenticationType' + } + ], + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.authType, + valueType: ServiceOptionType.string + } + ] + } + }; + capabilitiesService.fireCapabilitiesRegistered(Constants.mssqlProviderName, capabilitiesService.capabilities[Constants.mssqlProviderName]); + testConnectionParams = { + connectionType: ConnectionType.editor, + input: { + uri: 'test_uri', + onConnectStart: undefined, + onConnectSuccess: undefined, + onConnectReject: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined + }, + runQueryOnCompletion: undefined, + querySelection: undefined, + providers: ['MSSQL'] + }; + connectionProfile = createConnectionProfile('test_id'); + connectionProfile.providerName = undefined; + + mockConnectionManagementService.setup(x => x.getRecentConnections(TypeMoq.It.isValue(testConnectionParams.providers))).returns(() => { + return [connectionProfile]; + }); + mockConnectionManagementService.setup(x => x.addSavedPassword(TypeMoq.It.isAny())).returns(() => { + return Promise.resolve(connectionProfile); + }); + mockWidget = TypeMoq.Mock.ofType(ConnectionWidget, TypeMoq.MockBehavior.Strict, [], undefined, 'MSSQL'); + mockWidget.setup(x => x.focusOnOpen()); + mockWidget.setup(x => x.handleOnConnecting()); + mockWidget.setup(x => x.handleResetConnection()); + mockWidget.setup(x => x.connect(TypeMoq.It.isValue(connectionProfile))).returns(() => true); + mockWidget.setup(x => x.createConnectionWidget(TypeMoq.It.isAny())); + mockWidget.setup(x => x.updateServerGroup(TypeMoq.It.isAny())); + mockWidget.setup(x => x.initDialog(TypeMoq.It.isAny())); + mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(ConnectionWidget), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAnyString())).returns(() => { + return mockWidget.object; + }); + mockWidget.setup(x => x.DefaultServerGroup).returns(() => { + return { + id: '', + name: localize('defaultServerGroup', ""), + parentId: undefined, + color: undefined, + description: undefined, + }; + }); + mockWidget.setup(x => x.NoneServerGroup).returns(() => { + return { + id: '', + name: localize('noneServerGroup', ""), + parentId: undefined, + color: undefined, + description: undefined, + }; + }); + mockWidget.setup(x => x.databaseDropdownExpanded).returns(() => false); + mockWidget.setup(x => x.databaseDropdownExpanded = false); + + mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(ClearRecentConnectionsAction), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => { + return testInstantiationService.createInstance(ClearRecentConnectionsAction, ClearRecentConnectionsAction.ID, ClearRecentConnectionsAction.LABEL); + }); + mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(RecentConnectionActionsProvider))).returns(() => { + return testInstantiationService.createInstance(RecentConnectionActionsProvider); + }); + mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(RecentConnectionDataSource))).returns(() => { + return testInstantiationService.createInstance(RecentConnectionDataSource); + }); + mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(ServerTreeRenderer), true)).returns(() => { + return testInstantiationService.createInstance(ServerTreeRenderer, true); + }); + mockInstantationService.setup(x => x.createInstance(TypeMoq.It.isValue(RecentConnectionsDragAndDrop))).returns(() => { + return testInstantiationService.createInstance(RecentConnectionsDragAndDrop); + }); + }); + + teardown(() => { + ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); }); function getMockErrorMessageService(): TypeMoq.Mock { @@ -73,7 +235,7 @@ suite('ConnectionDialogService tests', () => { onConnectSuccess: undefined, onConnectReject: undefined, onDisconnect: undefined, - onConnectCanceled: undefined + onConnectCanceled: function () { } }, runQueryOnCompletion: undefined, querySelection: undefined @@ -99,4 +261,87 @@ suite('ConnectionDialogService tests', () => { test('handleDefaultOnConnect uses undefined URI for non-editor connections', () => { return testHandleDefaultOnConnectUri(false); }); + + test('openDialogAndWait should return a deferred promise when called', async () => { + // connectionResult is used for testing showErrorDialog. + let connectionResult: IConnectionResult = { + connected: false, + errorMessage: 'test_error', + errorCode: -1, + callStack: 'testCallStack' + }; + // promise only resolves upon handleDefaultOnConnect, must return it at the end + let connectionPromise = connectionDialogService.openDialogAndWait(mockConnectionManagementService.object, testConnectionParams, connectionProfile, connectionResult, false); + + /* handleDefaultOnConnect should reset connection and resolve properly + Also openDialogAndWait returns the connection profile passed in */ + (connectionDialogService as any).handleDefaultOnConnect(testConnectionParams, connectionProfile); + let result = await connectionPromise; + assert.equal(result, connectionProfile); + }); + + test('handleFillInConnectionInputs calls function on ConnectionController widget', async () => { + let called = false; + mockWidget.setup(x => x.fillInConnectionInputs(TypeMoq.It.isAny())).returns(() => { + called = true; + }); + await connectionDialogService.showDialog(mockConnectionManagementService.object, testConnectionParams, connectionProfile); + await (connectionDialogService as any).handleFillInConnectionInputs(connectionProfile); + let returnedModel = ((connectionDialogService as any)._connectionControllerMap['MSSQL'] as any)._model; + assert.equal(returnedModel._groupName, 'testGroup'); + assert(called); + }); + + test('handleOnConnect calls connectAndSaveProfile when called with profile', async () => { + let called = false; + mockConnectionManagementService.setup(x => x.connectAndSaveProfile(TypeMoq.It.isAny(), TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + called = true; + return Promise.resolve({ connected: true, errorMessage: undefined, errorCode: undefined }); + }); + + (connectionDialogService as any)._connectionDialog = undefined; + (connectionDialogService as any)._dialogDeferredPromise = new Deferred(); + await connectionDialogService.showDialog(mockConnectionManagementService.object, testConnectionParams, connectionProfile).then(() => { + ((connectionDialogService as any)._connectionControllerMap['MSSQL'] as any)._model = connectionProfile; + (connectionDialogService as any)._connectionDialog.connectButtonState = true; + ((connectionDialogService as any)._connectionDialog as any).connect(connectionProfile); + }); + + assert(called); + }); + + test('handleOnConnect calls connectAndSaveProfile when called without profile', async () => { + let called = false; + mockConnectionManagementService.setup(x => x.connectAndSaveProfile(TypeMoq.It.isAny(), TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + called = true; + return Promise.resolve({ connected: true, errorMessage: undefined, errorCode: undefined }); + }); + + (connectionDialogService as any)._connectionDialog = undefined; + (connectionDialogService as any)._dialogDeferredPromise = new Deferred(); + await connectionDialogService.showDialog(mockConnectionManagementService.object, testConnectionParams, connectionProfile).then(() => { + ((connectionDialogService as any)._connectionControllerMap['MSSQL'] as any)._model = connectionProfile; + (connectionDialogService as any)._connectionDialog.connectButtonState = true; + ((connectionDialogService as any)._connectionDialog as any).connect(); + }); + + assert(called); + }); + + test('handleOnCancel calls cancelEditorConnection', async () => { + let called = false; + mockConnectionManagementService.setup(x => x.cancelEditorConnection(TypeMoq.It.isAny())).returns(() => { + called = true; + return Promise.resolve(true); + }); + + (connectionDialogService as any)._connectionDialog = undefined; + (connectionDialogService as any)._dialogDeferredPromise = new Deferred(); + await connectionDialogService.showDialog(mockConnectionManagementService.object, testConnectionParams, connectionProfile).then(() => { + ((connectionDialogService as any)._connectionControllerMap['MSSQL'] as any)._model = connectionProfile; + ((connectionDialogService as any)._connectionDialog as any).cancel(); + }); + mockWidget.verify(x => x.databaseDropdownExpanded = false, TypeMoq.Times.atLeastOnce()); + assert(called); + }); }); diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts new file mode 100644 index 0000000000..fd1f364e8f --- /dev/null +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogWidget.test.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as TypeMoq from 'typemoq'; +import * as assert from 'assert'; +import * as DOM from 'vs/base/browser/dom'; +import * as Constants from 'sql/platform/connection/common/constants'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { ConnectionManagementService } from 'sql/workbench/services/connection/browser/connectionManagementService'; +import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { TestConnectionDialogWidget } from 'sql/workbench/services/connection/test/browser/testConnectionDialogWidget'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { entries } from 'sql/base/common/collections'; +import { TestLayoutService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { INewConnectionParams, ConnectionType, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; +import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService'; +import { createConnectionProfile } from 'sql/workbench/services/connection/test/browser/connectionManagementService.test'; +import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService'; +import { ViewDescriptorService } from 'vs/workbench/services/views/browser/viewDescriptorService'; +import { ViewContainer, Extensions, IViewsRegistry, IViewContainersRegistry, ITreeViewDescriptor, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { TestTreeView } from 'sql/workbench/services/connection/test/browser/testTreeView'; +suite('ConnectionDialogWidget tests', () => { + const testTreeViewId = 'testTreeView'; + const ViewsRegistry = Registry.as(Extensions.ViewsRegistry); + + let connectionDialogWidget: TestConnectionDialogWidget; + let mockConnectionManagementService: TypeMoq.Mock; + let cmInstantiationService: TestInstantiationService; + let element: HTMLElement; + let container: ViewContainer; + + setup(() => { + const viewInstantiationService: TestInstantiationService = workbenchInstantiationService(); + const viewDescriptorService = viewInstantiationService.createInstance(ViewDescriptorService); + container = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: 'testContainer', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + viewInstantiationService.stub(IViewDescriptorService, viewDescriptorService); + const viewDescriptor: ITreeViewDescriptor = { + id: testTreeViewId, + ctorDescriptor: null!, + name: 'Test View 1', + treeView: viewInstantiationService.createInstance(TestTreeView, 'testTree', 'Test Title'), + }; + ViewsRegistry.registerViews([viewDescriptor], container); + cmInstantiationService = new TestInstantiationService(); + cmInstantiationService.stub(IStorageService, new TestStorageService()); + mockConnectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Strict, + undefined, // connection store + undefined, // connection status manager + undefined, // connection dialog service + cmInstantiationService, // instantiation service + undefined, // editor service + undefined, // telemetry service + undefined, // configuration service + new TestCapabilitiesService()); + let providerDisplayNames = ['Mock SQL Server']; + let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' }; + connectionDialogWidget = new TestConnectionDialogWidget(providerDisplayNames, providerNameToDisplayMap['MSSQL'], providerNameToDisplayMap, cmInstantiationService, mockConnectionManagementService.object, undefined, undefined, viewDescriptorService, new TestThemeService(), new TestLayoutService(), new NullAdsTelemetryService(), new MockContextKeyService(), undefined, new NullLogService(), new TestTextResourcePropertiesService(new TestConfigurationService()), new TestConfigurationService()); + element = DOM.createStyleSheet(); + connectionDialogWidget.render(); + connectionDialogWidget.renderBody(element); + }); + + teardown(() => { + ViewsRegistry.deregisterViews(ViewsRegistry.getViews(container), container); + }); + + test('renderBody should have attached a connection dialog body onto element', () => { + assert.equal(element.childElementCount, 1); + assert.equal(element.children[0].className, 'connection-dialog'); + }); + + test('updateConnectionProviders should update connection providers', () => { + let providerDisplayNames = ['Mock SQL Server', 'Mock SQL Server 1']; + let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server', 'PGSQL': 'Mock SQL Server 1' }; + mockConnectionManagementService.setup(x => x.getUniqueConnectionProvidersByNameMap(TypeMoq.It.isAny())).returns(() => { + return getUniqueConnectionProvidersByNameMap(providerNameToDisplayMap); + }); + connectionDialogWidget.updateConnectionProviders(providerDisplayNames, providerNameToDisplayMap); + assert.equal(connectionDialogWidget.getDisplayNameFromProviderName('PGSQL'), providerNameToDisplayMap['PGSQL']); + }); + + test('setting newConnectionParams test for connectionDialogWidget', () => { + let params: INewConnectionParams = { + connectionType: ConnectionType.editor, + input: { + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectSuccess: undefined, + onConnectCanceled: undefined, + uri: 'Editor Uri' + }, + runQueryOnCompletion: RunQueryOnConnectionMode.executeQuery, + providers: ['MSSQL'] + }; + let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' }; + mockConnectionManagementService.setup(x => x.getUniqueConnectionProvidersByNameMap(TypeMoq.It.isAny())).returns(() => { + return getUniqueConnectionProvidersByNameMap(providerNameToDisplayMap); + }); + connectionDialogWidget.newConnectionParams = params; + assert.equal(connectionDialogWidget.newConnectionParams, params); + }); + + test('open should call onInitDialog', async () => { + let params: INewConnectionParams = { + connectionType: ConnectionType.editor, + input: { + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectSuccess: undefined, + onConnectCanceled: undefined, + uri: 'Editor Uri' + }, + runQueryOnCompletion: RunQueryOnConnectionMode.executeQuery, + providers: ['MSSQL'] + }; + let providerNameToDisplayMap = { 'MSSQL': 'Mock SQL Server' }; + mockConnectionManagementService.setup(x => x.getUniqueConnectionProvidersByNameMap(TypeMoq.It.isAny())).returns(() => { + return getUniqueConnectionProvidersByNameMap(providerNameToDisplayMap); + }); + //params must be assigned to get load providers + connectionDialogWidget.newConnectionParams = params; + let mockConnectionProfile = createConnectionProfile('test_id', ''); + mockConnectionManagementService.setup(x => x.getRecentConnections(TypeMoq.It.isValue(params.providers))).returns(() => { + return [mockConnectionProfile]; + }); + let called = false; + connectionDialogWidget.onInitDialog(() => { + called = true; + }); + await connectionDialogWidget.open(true); + assert(called); + called = false; + await connectionDialogWidget.open(false); + assert(called); + }); + + test('get set tests for connectButtonState and databaseDropdownExpanded', () => { + connectionDialogWidget.connectButtonState = true; + assert(connectionDialogWidget.connectButtonState); + connectionDialogWidget.databaseDropdownExpanded = true; + assert(connectionDialogWidget.databaseDropdownExpanded); + }); + + test('close/resetConnection should fire onRecentConnect', () => { + let called = false; + connectionDialogWidget.onResetConnection(() => { + called = true; + }); + connectionDialogWidget.close(); + assert(called); + }); + + test('updateProvider should call onShowUiComponent and onInitDialog', () => { + let returnedDisplayName: string; + let returnedContainer: HTMLElement; + let called = false; + connectionDialogWidget.onInitDialog(() => { + called = true; + }); + connectionDialogWidget.onShowUiComponent(e => { + returnedDisplayName = e.selectedProviderDisplayName; + returnedContainer = e.container; + }); + let providerDisplayName = 'Mock SQL Server'; + connectionDialogWidget.updateProvider(providerDisplayName); + assert.equal(returnedDisplayName, providerDisplayName); + assert.equal(returnedContainer.className, 'connection-provider-info'); + assert(called); + }); +}); + +// Copy of function in connectionManagementService. +export function getUniqueConnectionProvidersByNameMap(providerNameToDisplayNameMap: { [providerDisplayName: string]: string }): { [providerDisplayName: string]: string } { + let uniqueProvidersMap = {}; + let providerNames = entries(providerNameToDisplayNameMap); + providerNames.forEach(p => { + // Only add CMS provider if explicitly called from CMS extension + // otherwise avoid duplicate listing in dropdown + if (p[0] !== Constants.cmsProviderName) { + uniqueProvidersMap[p[0]] = p[1]; + } else { + if (providerNames.length === 1) { + uniqueProvidersMap[p[0]] = p[1]; + } + } + }); + + return uniqueProvidersMap; +} diff --git a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts index 141c29fab3..5b4d0082b5 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts @@ -252,13 +252,12 @@ suite('SQL ConnectionManagementService tests', () => { } } - test('showConnectionDialog should open the dialog with default type given no parameters', () => { - return connectionManagementService.showConnectionDialog().then(() => { - verifyShowConnectionDialog(undefined, ConnectionType.default, undefined, false); - }); + test('showConnectionDialog should open the dialog with default type given no parameters', async () => { + await connectionManagementService.showConnectionDialog(); + verifyShowConnectionDialog(undefined, ConnectionType.default, undefined, false); }); - test('showConnectionDialog should open the dialog with given type given valid input', () => { + test('showConnectionDialog should open the dialog with given type given valid input', async () => { let params: INewConnectionParams = { connectionType: ConnectionType.editor, input: { @@ -271,12 +270,12 @@ suite('SQL ConnectionManagementService tests', () => { }, runQueryOnCompletion: RunQueryOnConnectionMode.executeQuery }; - return connectionManagementService.showConnectionDialog(params).then(() => { - verifyShowConnectionDialog(undefined, params.connectionType, params.input.uri, false); - }); + await connectionManagementService.showConnectionDialog(params); + verifyShowConnectionDialog(undefined, params.connectionType, params.input.uri, false); + }); - test('showConnectionDialog should pass the model to the dialog if there is a model assigned to the uri', () => { + test('showConnectionDialog should pass the model to the dialog if there is a model assigned to the uri', async () => { let params: INewConnectionParams = { connectionType: ConnectionType.editor, input: { @@ -290,24 +289,21 @@ suite('SQL ConnectionManagementService tests', () => { runQueryOnCompletion: RunQueryOnConnectionMode.executeQuery }; - return connect(params.input.uri).then(() => { - let saveConnection = connectionManagementService.getConnectionProfile(params.input.uri); + await connect(params.input.uri); + let saveConnection = connectionManagementService.getConnectionProfile(params.input.uri); - assert.notEqual(saveConnection, undefined, `profile was not added to the connections`); - assert.equal(saveConnection.serverName, connectionProfile.serverName, `Server names are different`); - return connectionManagementService.showConnectionDialog(params).then(() => { - verifyShowConnectionDialog(connectionProfile, params.connectionType, params.input.uri, false); - }); - }); + assert.notEqual(saveConnection, undefined, `profile was not added to the connections`); + assert.equal(saveConnection.serverName, connectionProfile.serverName, `Server names are different`); + await connectionManagementService.showConnectionDialog(params); + verifyShowConnectionDialog(connectionProfile, params.connectionType, params.input.uri, false); }); - test('showConnectionDialog should not be called when using showEditConnectionDialog', () => { - return connectionManagementService.showEditConnectionDialog(connectionProfile).then(() => { - verifyShowConnectionDialog(connectionProfile, ConnectionType.default, undefined, false, undefined, false); - }); + test('showConnectionDialog should not be called when using showEditConnectionDialog', async () => { + await connectionManagementService.showEditConnectionDialog(connectionProfile); + verifyShowConnectionDialog(connectionProfile, ConnectionType.default, undefined, false, undefined, false); }); - test('connect should save profile given options with saveProfile set to true', () => { + test('connect should save profile given options with saveProfile set to true', async () => { let uri: string = 'Editor Uri'; let options: IConnectionCompletionOptions = { params: undefined, @@ -317,9 +313,8 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri, options).then(() => { - verifyOptions(options); - }); + await connect(uri, options); + verifyOptions(options); }); test('getDefaultProviderId is MSSQL', () => { @@ -329,24 +324,21 @@ suite('SQL ConnectionManagementService tests', () => { /* Andresse 10/5/17 commented this test out since it was only working before my changes by the chance of how Promises work If we want to continue to test this, the connection logic needs to be rewritten to actually wait for everything to be done before it resolves */ - // test('connect should show dashboard given options with showDashboard set to true', done => { + // test('connect should show dashboard given options with showDashboard set to true', async () => { // let uri: string = 'Editor Uri'; // let options: IConnectionCompletionOptions = { // params: undefined, // saveTheConnection: false, // showDashboard: true, - // showConnectionDialogOnError: false + // showConnectionDialogOnError: false, + // showFirewallRuleOnError: false // }; - // connect(uri, options).then(() => { - // verifyOptions(options); - // done(); - // }).catch(err => { - // done(err); - // }); + // await connect(uri, options); + // verifyOptions(options); // }); - test('connect should pass the params in options to onConnectSuccess callback', () => { + test('connect should pass the params in options to onConnectSuccess callback', async () => { let uri: string = 'Editor Uri'; let paramsInOnConnectSuccess: INewConnectionParams; let options: IConnectionCompletionOptions = { @@ -371,32 +363,29 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri, options).then((result) => { - verifyOptions(options); - assert.notEqual(paramsInOnConnectSuccess, undefined); - assert.equal(paramsInOnConnectSuccess.connectionType, options.params.connectionType); - }); + await connect(uri, options); + verifyOptions(options); + assert.notEqual(paramsInOnConnectSuccess, undefined); + assert.equal(paramsInOnConnectSuccess.connectionType, options.params.connectionType); }); - test('connectAndSaveProfile should show not load the password', () => { + test('connectAndSaveProfile should show not load the password', async () => { let uri: string = 'Editor Uri'; let options: IConnectionCompletionOptions = undefined; - return connect(uri, options, true).then(() => { - verifyOptions(options, true); - }); + await connect(uri, options, true); + verifyOptions(options, true); }); - test('connect with undefined uri and options should connect using the default uri', () => { + test('connect with undefined uri and options should connect using the default uri', async () => { let uri = undefined; let options: IConnectionCompletionOptions = undefined; - return connect(uri, options).then(() => { - assert.equal(connectionManagementService.isProfileConnected(connectionProfile), true); - }); + await connect(uri, options); + assert.equal(connectionManagementService.isProfileConnected(connectionProfile), true); }); - test('failed connection should open the dialog if connection fails', () => { + test('failed connection should open the dialog if connection fails', async () => { let uri = undefined; let error: string = 'error'; let errorCode: number = 111; @@ -417,15 +406,14 @@ suite('SQL ConnectionManagementService tests', () => { callStack: errorCallStack }; - return connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, connectionResult.errorMessage); - verifyShowFirewallRuleDialog(connectionProfile, false); - verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult); - }); + let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, connectionResult.errorMessage); + verifyShowFirewallRuleDialog(connectionProfile, false); + verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult); }); - test('failed connection should not open the dialog if the option is set to false even if connection fails', () => { + test('failed connection should not open the dialog if the option is set to false even if connection fails', async () => { let uri = undefined; let error: string = 'error when options set to false'; let errorCode: number = 111; @@ -446,12 +434,11 @@ suite('SQL ConnectionManagementService tests', () => { callStack: errorCallStack }; - return connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, connectionResult.errorMessage); - verifyShowFirewallRuleDialog(connectionProfile, false); - verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, false); - }); + let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, connectionResult.errorMessage); + verifyShowFirewallRuleDialog(connectionProfile, false); + verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, false); }); test('Accessors for event emitters should return emitter function', () => { @@ -463,7 +450,7 @@ suite('SQL ConnectionManagementService tests', () => { assert.equal(typeof (onConnect1), 'function'); }); - test('onConnectionChangedNotification should call onConnectionChanged event', () => { + test('onConnectionChangedNotification should call onConnectionChanged event', async () => { let uri = 'Test Uri'; let options: IConnectionCompletionOptions = { params: undefined, @@ -473,32 +460,30 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri, options).then(result => { - let saveConnection = connectionManagementService.getConnectionProfile(uri); - let changedConnectionInfo: azdata.ChangedConnectionInfo = { connectionUri: uri, connection: saveConnection }; - let called = false; - connectionManagementService.onConnectionChanged((params: IConnectionParams) => { - assert.equal(uri, params.connectionUri); - assert.equal(saveConnection, params.connectionProfile); - called = true; - }); - connectionManagementService.onConnectionChangedNotification(0, changedConnectionInfo); - assert.ok(called, 'expected onConnectionChanged event to be sent'); + await connect(uri, options); + let saveConnection = connectionManagementService.getConnectionProfile(uri); + let changedConnectionInfo: azdata.ChangedConnectionInfo = { connectionUri: uri, connection: saveConnection }; + let called = false; + connectionManagementService.onConnectionChanged((params: IConnectionParams) => { + assert.equal(uri, params.connectionUri); + assert.equal(saveConnection, params.connectionProfile); + called = true; }); + connectionManagementService.onConnectionChangedNotification(0, changedConnectionInfo); + assert.ok(called, 'expected onConnectionChanged event to be sent'); }); - test('changeGroupIdForconnection should change the groupId for a connection profile', () => { + test('changeGroupIdForconnection should change the groupId for a connection profile', async () => { let profile = assign({}, connectionProfile); profile.options = { password: profile.password }; profile.id = 'test_id'; let newGroupId = 'new_group_id'; connectionStore.setup(x => x.changeGroupIdForConnection(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve()); - connectionManagementService.changeGroupIdForConnection(profile, newGroupId).then(() => { - assert.equal(profile.groupId, newGroupId); - }); + await connectionManagementService.changeGroupIdForConnection(profile, newGroupId); + assert.equal(profile.groupId, newGroupId); }); - test('changeGroupIdForConnectionGroup should call changeGroupIdForConnectionGroup in ConnectionStore', () => { + test('changeGroupIdForConnectionGroup should call changeGroupIdForConnectionGroup in ConnectionStore', async () => { let sourceProfileGroup = createConnectionGroup('original_id'); let targetProfileGroup = createConnectionGroup('new_id'); let called = false; @@ -506,12 +491,11 @@ suite('SQL ConnectionManagementService tests', () => { called = true; return Promise.resolve(); }); - connectionManagementService.changeGroupIdForConnectionGroup(sourceProfileGroup, targetProfileGroup).then(() => { - assert.ok(called, 'expected changeGroupIdForConnectionGroup to be called on ConnectionStore'); - }); + await connectionManagementService.changeGroupIdForConnectionGroup(sourceProfileGroup, targetProfileGroup); + assert.ok(called, 'expected changeGroupIdForConnectionGroup to be called on ConnectionStore'); }); - test('findExistingConnection should find connection for connectionProfile with same info', () => { + test('findExistingConnection should find connection for connectionProfile with same info', async () => { let profile = assign({}, connectionProfile); let uri1 = 'connection:connectionId'; let options: IConnectionCompletionOptions = { @@ -537,13 +521,12 @@ suite('SQL ConnectionManagementService tests', () => { let connectionInfoString = 'providerName:' + profile.providerName + '|authenticationType:' + profile.authenticationType + '|databaseName:' + profile.databaseName + '|serverName:' + profile.serverName + '|userName:' + profile.userName; - return connect(uri1, options, true, profile).then(() => { - let returnedProfile = connectionManagementService.findExistingConnection(profile); - assert.equal(returnedProfile.getConnectionInfoId(), connectionInfoString); - }); + await connect(uri1, options, true, profile); + let returnedProfile = connectionManagementService.findExistingConnection(profile); + assert.equal(returnedProfile.getConnectionInfoId(), connectionInfoString); }); - test('deleteConnection should delete the connection properly', () => { + test('deleteConnection should delete the connection properly', async () => { let profile = assign({}, connectionProfile); let uri1 = 'connection:connectionId'; let options: IConnectionCompletionOptions = { @@ -568,12 +551,13 @@ suite('SQL ConnectionManagementService tests', () => { }; connectionStore.setup(x => x.deleteConnectionFromConfiguration(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - return connect(uri1, options, true, profile).then(() => { - assert(connectionManagementService.deleteConnection(profile)); - }); + // deleteConnection should work for profile not connected. + assert(connectionManagementService.deleteConnection(profile)); + await connect(uri1, options, true, profile); + assert(connectionManagementService.deleteConnection(profile)); }); - test('deleteConnectionGroup should delete connections in connection group', () => { + test('deleteConnectionGroup should delete connections in connection group', async () => { let profile = assign({}, connectionProfile); let profileGroup = createConnectionGroup('original_id'); profileGroup.addConnections([profile]); @@ -600,11 +584,9 @@ suite('SQL ConnectionManagementService tests', () => { }; connectionStore.setup(x => x.deleteGroupFromConfiguration(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - return connect(uri1, options, true, profile).then(() => { - return connectionManagementService.deleteConnectionGroup(profileGroup).then(result => { - assert(result); - }); - }); + await connect(uri1, options, true, profile); + let result = await connectionManagementService.deleteConnectionGroup(profileGroup); + assert(result); }); test('canChangeConnectionConfig returns true when connection can be moved to another group', () => { @@ -616,9 +598,9 @@ suite('SQL ConnectionManagementService tests', () => { assert(connectionManagementService.canChangeConnectionConfig(profile, newGroupId)); }); - test('connectIfNotConnected should not try to connect with already connected profile', () => { + test('isProfileConnecting should return false for already connected profile', async () => { let profile = assign({}, connectionProfile); - let uri1 = 'connection:connectionId'; //must use default connection uri for test to work. + let uri = 'Editor Uri'; let options: IConnectionCompletionOptions = { params: { connectionType: ConnectionType.editor, @@ -628,7 +610,7 @@ suite('SQL ConnectionManagementService tests', () => { onConnectStart: undefined, onDisconnect: undefined, onConnectCanceled: undefined, - uri: uri1, + uri: uri, }, queryRange: undefined, runQueryOnCompletion: RunQueryOnConnectionMode.none, @@ -640,15 +622,349 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri1, options, true, profile).then(result => { - assert.equal(result.connected, true); - return connectionManagementService.connectIfNotConnected(profile, undefined, true).then(result => { - assert.equal(result, uri1); - }); - }); + await connect(uri, options, true, profile); + assert(!connectionManagementService.isProfileConnecting(profile)); }); - test('Edit Connection - Changing connection profile name for same URI should persist after edit', () => { + test('disconnect should disconnect the profile when given ConnectionProfile', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + await connect(uri, options, true, profile); + await connectionManagementService.disconnect(profile); + assert(!connectionManagementService.isProfileConnected(profile)); + }); + + test('disconnect should disconnect the profile when given uri string', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + await connect(uri, options, true, profile); + await connectionManagementService.disconnect(uri); + assert(!connectionManagementService.isProfileConnected(profile)); + }); + + test('cancelConnection should disconnect the profile', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + await connect(uri, options, true, profile); + await connectionManagementService.cancelConnection(profile); + assert(!connectionManagementService.isConnected(undefined, profile)); + }); + + test('cancelEditorConnection should not delete editor connection when already connected', async () => { + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + await connect(uri, options); + let result = await connectionManagementService.cancelEditorConnection(options.params.input); + assert.equal(result, false); + assert(connectionManagementService.isConnected(uri)); + }); + + test('getConnection should grab connection that is connected', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let badString = 'bad_string'; + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + await connect(uri, options, true, profile); + // invalid uri check. + assert.equal(connectionManagementService.getConnection(badString), undefined); + let returnedProfile = connectionManagementService.getConnection(uri); + assert.equal(returnedProfile.groupFullName, profile.groupFullName); + assert.equal(returnedProfile.groupId, profile.groupId); + }); + + test('connectIfNotConnected should not try to connect with already connected profile', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + await connect(uri, options, true, profile); + let result = await connectionManagementService.connectIfNotConnected(profile, undefined, true); + assert.equal(result, uri); + }); + + test('getServerInfo should return undefined when given an invalid string', () => { + let badString = 'bad_string'; + assert.equal(connectionManagementService.getServerInfo(badString), undefined); + }); + + test('getConnectionString should get connection string of connectionId', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let badString = 'bad_string'; + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + let getConnectionResult = await connectionManagementService.getConnectionString(badString); + // test for invalid profile id + assert.equal(getConnectionResult, undefined); + await connect(uri, options, true, profile); + let currentConnections = connectionManagementService.getConnections(true); + let profileId = currentConnections[0].id; + let testConnectionString = 'test_connection_string'; + mssqlConnectionProvider.setup(x => x.getConnectionString(uri, false)).returns(() => { + return Promise.resolve(testConnectionString); + }); + getConnectionResult = await connectionManagementService.getConnectionString(profileId, false); + assert(getConnectionResult, testConnectionString); + }); + + + test('rebuildIntellisenseCache should call rebuildIntelliSenseCache on provider', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + let cacheRebuilt = false; + mssqlConnectionProvider.setup(x => x.rebuildIntelliSenseCache(uri)).returns(() => { + cacheRebuilt = true; + return Promise.resolve(); + }); + await assert.rejects(async () => await connectionManagementService.rebuildIntelliSenseCache(uri)); + await connect(uri, options, true, profile); + await connectionManagementService.rebuildIntelliSenseCache(uri); + assert(cacheRebuilt); + }); + + test('buildConnectionInfo should get connection string of connectionId', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + + let providerName = 'MSSQL'; + let testConnectionString = 'test_connection_string'; + mssqlConnectionProvider.setup(x => x.buildConnectionInfo('test_connection_string')).returns(() => { + let ConnectionInfo: azdata.ConnectionInfo = { options: options }; + return Promise.resolve(ConnectionInfo); + }); + await connect(uri, options, true, profile); + let result = await connectionManagementService.buildConnectionInfo(testConnectionString, providerName); + assert.equal(result.options, options); + }); + + test('removeConnectionProfileCredentials should return connection profile without password', () => { + let profile = assign({}, connectionProfile); + connectionStore.setup(x => x.getProfileWithoutPassword(TypeMoq.It.isAny())).returns(() => { + let profileWithoutPass = assign({}, connectionProfile); + profileWithoutPass.password = undefined; + return profileWithoutPass; + }); + let clearedProfile = connectionManagementService.removeConnectionProfileCredentials(profile); + assert.equal(clearedProfile.password, undefined); + }); + + test('getConnectionProfileById should return profile when given profileId', async () => { + let profile = assign({}, connectionProfile); + let uri = 'connection:connectionId'; // must use default connection uri for test to work. + let badString = 'bad_string'; + let options: IConnectionCompletionOptions = { + params: { + connectionType: ConnectionType.editor, + input: { + onConnectSuccess: undefined, + onConnectReject: undefined, + onConnectStart: undefined, + onDisconnect: undefined, + onConnectCanceled: undefined, + uri: uri, + }, + queryRange: undefined, + runQueryOnCompletion: RunQueryOnConnectionMode.none, + isEditConnection: false + }, + saveTheConnection: true, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + let result = await connect(uri, options, true, profile); + assert.equal(result.connected, true); + assert.equal(connectionManagementService.getConnectionProfileById(badString), undefined); + let currentConnections = connectionManagementService.getConnections(true); + let profileId = currentConnections[0].id; + let returnedProfile = connectionManagementService.getConnectionProfileById(profileId); + assert.equal(returnedProfile.groupFullName, profile.groupFullName); + assert.equal(returnedProfile.groupId, profile.groupId); + }); + + test('Edit Connection - Changing connection profile name for same URI should persist after edit', async () => { let profile = assign({}, connectionProfile); let uri1 = 'test_uri1'; let newname = 'connection renamed'; @@ -673,19 +989,15 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri1, options, true, profile).then(result => { - assert.equal(result.connected, true); - let newProfile = assign({}, connectionProfile); - newProfile.connectionName = newname; - options.params.isEditConnection = true; - return connect(uri1, options, true, newProfile).then(result => { - assert.equal(result.connected, true); - assert.equal(connectionManagementService.getConnectionProfile(uri1).connectionName, newname); - }); - }); + await connect(uri1, options, true, profile); + let newProfile = assign({}, connectionProfile); + newProfile.connectionName = newname; + options.params.isEditConnection = true; + await connect(uri1, options, true, newProfile); + assert.equal(connectionManagementService.getConnectionProfile(uri1).connectionName, newname); }); - test('Edit Connection - Connecting a different URI with same profile via edit should not change profile ID.', () => { + test('Edit Connection - Connecting a different URI with same profile via edit should not change profile ID.', async () => { let uri1 = 'test_uri1'; let uri2 = 'test_uri2'; let profile = assign({}, connectionProfile); @@ -711,20 +1023,16 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri1, options, true, profile).then(result => { - assert.equal(result.connected, true); - options.params.isEditConnection = true; - return connect(uri2, options, true, profile).then(result => { - assert.equal(result.connected, true); - let uri1info = connectionManagementService.getConnectionInfo(uri1); - let uri2info = connectionManagementService.getConnectionInfo(uri2); - assert.equal(uri1info.connectionProfile.id, uri2info.connectionProfile.id); - }); - }); + await connect(uri1, options, true, profile); + options.params.isEditConnection = true; + await connect(uri2, options, true, profile); + let uri1info = connectionManagementService.getConnectionInfo(uri1); + let uri2info = connectionManagementService.getConnectionInfo(uri2); + assert.equal(uri1info.connectionProfile.id, uri2info.connectionProfile.id); }); - test('failed firewall rule should open the firewall rule dialog', () => { + test('failed firewall rule should open the firewall rule dialog', async () => { handleFirewallRuleResult.canHandleFirewallRule = true; resolveHandleFirewallRuleDialog = true; isFirewallRuleAdded = true; @@ -742,14 +1050,13 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri, options, false, connectionProfile, error, errorCode).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, expectedError); - verifyShowFirewallRuleDialog(connectionProfile, true); - }); + let result = await connect(uri, options, false, connectionProfile, error, errorCode); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, expectedError); + verifyShowFirewallRuleDialog(connectionProfile, true); }); - test('failed firewall rule connection should not open the firewall rule dialog if the option is set to false even if connection fails', () => { + test('failed firewall rule connection should not open the firewall rule dialog if the option is set to false even if connection fails', async () => { handleFirewallRuleResult.canHandleFirewallRule = true; resolveHandleFirewallRuleDialog = true; isFirewallRuleAdded = true; @@ -774,18 +1081,21 @@ suite('SQL ConnectionManagementService tests', () => { callStack: errorCallStack }; - return connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, connectionResult.errorMessage); - verifyShowFirewallRuleDialog(connectionProfile, false); - verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, false); - }); + let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, connectionResult.errorMessage); + verifyShowFirewallRuleDialog(connectionProfile, false); + verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, false); }); test('hasRegisteredServers should return true as there is one registered server', () => { assert(connectionManagementService.hasRegisteredServers()); }); + test('getConnectionIconId should return undefined as there is no mementoObj service', () => { + let connectionId = 'connection:connectionId'; + assert.equal(connectionManagementService.getConnectionIconId(connectionId), undefined); + }); test('getAdvancedProperties should return a list of properties for connectionManagementService', () => { let propertyNames = ['connectionName', 'serverName', 'databaseName', 'userName', 'authenticationType', 'password']; @@ -798,32 +1108,35 @@ suite('SQL ConnectionManagementService tests', () => { assert.equal(propertyNames[5], advancedProperties[5].name); }); - test('saveProfileGroup should return groupId from connection group', () => { + test('saveProfileGroup should return groupId from connection group', async () => { let newConnectionGroup = createConnectionGroup(connectionProfile.groupId); connectionStore.setup(x => x.saveProfileGroup(TypeMoq.It.isAny())).returns(() => Promise.resolve(connectionProfile.groupId)); - connectionManagementService.saveProfileGroup(newConnectionGroup).then(result => { - assert.equal(result, connectionProfile.groupId); - }); + let result = await connectionManagementService.saveProfileGroup(newConnectionGroup); + assert.equal(result, connectionProfile.groupId); }); - test('editGroup should fire onAddConnectionProfile', () => { + test('editGroup should fire onAddConnectionProfile', async () => { let newConnectionGroup = createConnectionGroup(connectionProfile.groupId); let called = false; connectionStore.setup(x => x.editGroup(TypeMoq.It.isAny())).returns(() => Promise.resolve()); connectionManagementService.onAddConnectionProfile(() => { called = true; }); - return connectionManagementService.editGroup(newConnectionGroup).then(() => { - assert(called); - }); + await connectionManagementService.editGroup(newConnectionGroup); + assert(called); }); - test('getFormattedUri should return formatted uri when given default type uri', () => { + test('getFormattedUri should return formatted uri when given default type uri or already formatted uri', () => { let testUri = 'connection:'; - assert.equal('connection:connectionId', connectionManagementService.getFormattedUri(testUri, connectionProfile)); + let formattedUri = 'connection:connectionId'; + let badUri = 'bad_uri'; + assert.equal(formattedUri, connectionManagementService.getFormattedUri(testUri, connectionProfile)); + assert.equal(formattedUri, connectionManagementService.getFormattedUri(formattedUri, connectionProfile)); + // test for invalid URI + assert.equal(badUri, connectionManagementService.getFormattedUri(badUri, connectionProfile)); }); - test('failed firewall rule connection and failed during open firewall rule should open the firewall rule dialog and connection dialog with error', () => { + test('failed firewall rule connection and failed during open firewall rule should open the firewall rule dialog and connection dialog with error', async () => { handleFirewallRuleResult.canHandleFirewallRule = true; resolveHandleFirewallRuleDialog = true; isFirewallRuleAdded = true; @@ -848,15 +1161,14 @@ suite('SQL ConnectionManagementService tests', () => { callStack: errorCallStack }; - return connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, connectionResult.errorMessage); - verifyShowFirewallRuleDialog(connectionProfile, true); - verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, true); - }); + let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, connectionResult.errorMessage); + verifyShowFirewallRuleDialog(connectionProfile, true); + verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, true); }); - test('failed firewall rule connection should open the firewall rule dialog. Then canceled firewall rule dialog should not open connection dialog', () => { + test('failed firewall rule connection should open the firewall rule dialog. Then canceled firewall rule dialog should not open connection dialog', async () => { handleFirewallRuleResult.canHandleFirewallRule = true; resolveHandleFirewallRuleDialog = true; isFirewallRuleAdded = false; @@ -881,14 +1193,13 @@ suite('SQL ConnectionManagementService tests', () => { callStack: errorCallStack }; - return connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack).then(result => { - assert.equal(result, undefined); - verifyShowFirewallRuleDialog(connectionProfile, true); - verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, false); - }); + let result = await connect(uri, options, false, connectionProfile, error, errorCode, errorCallStack); + assert.equal(result, undefined); + verifyShowFirewallRuleDialog(connectionProfile, true); + verifyShowConnectionDialog(connectionProfile, ConnectionType.default, uri, true, connectionResult, false); }); - test('connect when password is empty and unsaved should open the dialog', () => { + test('connect when password is empty and unsaved should open the dialog', async () => { let uri = undefined; let expectedConnection: boolean = false; let options: IConnectionCompletionOptions = { @@ -906,15 +1217,14 @@ suite('SQL ConnectionManagementService tests', () => { callStack: undefined }; - return connect(uri, options, false, connectionProfileWithEmptyUnsavedPassword).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, connectionResult.errorMessage); - verifyShowConnectionDialog(connectionProfileWithEmptyUnsavedPassword, ConnectionType.default, uri, true, connectionResult); - verifyShowFirewallRuleDialog(connectionProfile, false); - }); + let result = await connect(uri, options, false, connectionProfileWithEmptyUnsavedPassword); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, connectionResult.errorMessage); + verifyShowConnectionDialog(connectionProfileWithEmptyUnsavedPassword, ConnectionType.default, uri, true, connectionResult); + verifyShowFirewallRuleDialog(connectionProfile, false); }); - test('connect when password is empty and saved should not open the dialog', () => { + test('connect when password is empty and saved should not open the dialog', async () => { let uri = undefined; let expectedConnection: boolean = true; let options: IConnectionCompletionOptions = { @@ -932,14 +1242,13 @@ suite('SQL ConnectionManagementService tests', () => { callStack: undefined }; - return connect(uri, options, false, connectionProfileWithEmptySavedPassword).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, connectionResult.errorMessage); - verifyShowConnectionDialog(connectionProfileWithEmptySavedPassword, ConnectionType.default, uri, true, connectionResult, false); - }); + let result = await connect(uri, options, false, connectionProfileWithEmptySavedPassword); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, connectionResult.errorMessage); + verifyShowConnectionDialog(connectionProfileWithEmptySavedPassword, ConnectionType.default, uri, true, connectionResult, false); }); - test('connect from editor when empty password when it is required and saved should not open the dialog', () => { + test('connect from editor when empty password when it is required and saved should not open the dialog', async () => { let uri = 'editor 3'; let expectedConnection: boolean = true; let options: IConnectionCompletionOptions = { @@ -969,14 +1278,13 @@ suite('SQL ConnectionManagementService tests', () => { callStack: undefined }; - return connect(uri, options, false, connectionProfileWithEmptySavedPassword).then(result => { - assert.equal(result.connected, expectedConnection); - assert.equal(result.errorMessage, connectionResult.errorMessage); - verifyShowConnectionDialog(connectionProfileWithEmptySavedPassword, ConnectionType.editor, uri, true, connectionResult, false); - }); + let result = await connect(uri, options, false, connectionProfileWithEmptySavedPassword); + assert.equal(result.connected, expectedConnection); + assert.equal(result.errorMessage, connectionResult.errorMessage); + verifyShowConnectionDialog(connectionProfileWithEmptySavedPassword, ConnectionType.editor, uri, true, connectionResult, false); }); - test('disconnect editor should disconnect uri from connection', () => { + test('disconnect editor should disconnect uri from connection', async () => { let uri = 'editor to remove'; let options: IConnectionCompletionOptions = { params: { @@ -998,14 +1306,12 @@ suite('SQL ConnectionManagementService tests', () => { showFirewallRuleOnError: true }; - return connect(uri, options, false, connectionProfileWithEmptySavedPassword).then(() => { - return connectionManagementService.disconnectEditor(options.params.input).then(result => { - assert(result); - }); - }); + await connect(uri, options, false, connectionProfileWithEmptySavedPassword); + let result = await connectionManagementService.disconnectEditor(options.params.input); + assert(result); }); - test('registerIconProvider should register icon provider for connectionManagementService', () => { + test('registerIconProvider should register icon provider for connectionManagementService', async () => { let profile = assign({}, connectionProfile); let serverInfo: azdata.ServerInfo = { serverMajorVersion: 0, @@ -1059,9 +1365,17 @@ suite('SQL ConnectionManagementService tests', () => { } }; connectionManagementService.registerIconProvider('MSSQL', mockIconProvider); - return connect(uri, options, true, profile, undefined, undefined, undefined, serverInfo).then(() => { - assert(called); - }); + await connect(uri, options, true, profile, undefined, undefined, undefined, serverInfo); + assert(called); + }); + + test('getProviderProperties should return properties of a provider in ConnectionManagementService', () => { + let mssqlId = 'MSSQL'; + let pgsqlId = 'PGSQL'; + let mssqlProperties = connectionManagementService.getProviderProperties('MSSQL'); + let pgsqlProperties = connectionManagementService.getProviderProperties('PGSQL'); + assert.equal(mssqlProperties.providerId, mssqlId); + assert.equal(pgsqlProperties.providerId, pgsqlId); }); test('doChangeLanguageFlavor should throw on unknown provider', () => { @@ -1106,7 +1420,17 @@ suite('SQL ConnectionManagementService tests', () => { assert.equal(providerNames[2], expectedNames[2]); }); - test('ensureDefaultLanguageFlavor should not send event if uri is connected', () => { + test('ensureDefaultLanguageFlavor should send event if uri is not connected', () => { + let uri: string = 'Test Uri'; + let called = false; + connectionManagementService.onLanguageFlavorChanged((changeParams: azdata.DidChangeLanguageFlavorParams) => { + called = true; + }); + connectionManagementService.ensureDefaultLanguageFlavor(uri); + assert(called); + }); + + test('ensureDefaultLanguageFlavor should not send event if uri is connected', async () => { let uri: string = 'Editor Uri'; let options: IConnectionCompletionOptions = { params: undefined, @@ -1119,14 +1443,13 @@ suite('SQL ConnectionManagementService tests', () => { connectionManagementService.onLanguageFlavorChanged((changeParams: azdata.DidChangeLanguageFlavorParams) => { called = true; }); - return connect(uri, options).then(() => { - called = false; //onLanguageFlavorChanged is called when connecting, must set back to false. - connectionManagementService.ensureDefaultLanguageFlavor(uri); - assert.equal(called, false, 'do not expect flavor change to be called'); - }); + await connect(uri, options); + called = false; //onLanguageFlavorChanged is called when connecting, must set back to false. + connectionManagementService.ensureDefaultLanguageFlavor(uri); + assert.equal(called, false, 'do not expect flavor change to be called'); }); - test('getConnectionId returns the URI associated with a connection that has had its database filled in', () => { + test('getConnectionId returns the URI associated with a connection that has had its database filled in', async () => { // Set up the connection management service with a connection corresponding to a default database let dbName = 'master'; let serverName = 'test_server'; @@ -1136,19 +1459,18 @@ suite('SQL ConnectionManagementService tests', () => { let connectionProfileWithDb: IConnectionProfile = assign(connectionProfileWithoutDb, { databaseName: dbName }); // Save the database with a URI that has the database name filled in, to mirror Carbon's behavior let ownerUri = Utils.generateUri(connectionProfileWithDb); - return connect(ownerUri, undefined, false, connectionProfileWithoutDb).then(() => { - // If I get the URI for the connection with or without a database from the connection management service - let actualUriWithDb = connectionManagementService.getConnectionUri(connectionProfileWithDb); - let actualUriWithoutDb = connectionManagementService.getConnectionUri(connectionProfileWithoutDb); + await connect(ownerUri, undefined, false, connectionProfileWithoutDb); + // If I get the URI for the connection with or without a database from the connection management service + let actualUriWithDb = connectionManagementService.getConnectionUri(connectionProfileWithDb); + let actualUriWithoutDb = connectionManagementService.getConnectionUri(connectionProfileWithoutDb); - // Then the retrieved URIs should match the one on the connection - let expectedUri = Utils.generateUri(connectionProfileWithoutDb); - assert.equal(actualUriWithDb, expectedUri); - assert.equal(actualUriWithoutDb, expectedUri); - }); + // Then the retrieved URIs should match the one on the connection + let expectedUri = Utils.generateUri(connectionProfileWithoutDb); + assert.equal(actualUriWithDb, expectedUri); + assert.equal(actualUriWithoutDb, expectedUri); }); - test('list and change database tests', () => { + test('list and change database tests', async () => { // Set up the connection management service with a connection corresponding to a default database let dbName = 'master'; let newDbName = 'renamed_master'; @@ -1175,16 +1497,22 @@ suite('SQL ConnectionManagementService tests', () => { }; mssqlConnectionProvider.setup(x => x.listDatabases(ownerUri)).returns(() => listDatabasesThenable(ownerUri)); mssqlConnectionProvider.setup(x => x.changeDatabase(ownerUri, newDbName)).returns(() => changeDatabasesThenable(ownerUri, newDbName)); - return connect(ownerUri, undefined, false, connectionProfileWithoutDb).then(() => { - return connectionManagementService.listDatabases(ownerUri).then(result => { - assert.equal(result.databaseNames.length, 1); - assert.equal(result.databaseNames[0], dbName); - return connectionManagementService.changeDatabase(ownerUri, newDbName).then(result => { - assert(result); - assert.equal(newDbName, connectionManagementService.getConnectionProfile(ownerUri).databaseName); - }); - }); - }); + await connect(ownerUri, undefined, false, connectionProfileWithoutDb); + let listDatabasesResult = await connectionManagementService.listDatabases(ownerUri); + assert.equal(listDatabasesResult.databaseNames.length, 1); + assert.equal(listDatabasesResult.databaseNames[0], dbName); + let changeDatabaseResults = await connectionManagementService.changeDatabase(ownerUri, newDbName); + assert(changeDatabaseResults); + assert.equal(newDbName, connectionManagementService.getConnectionProfile(ownerUri).databaseName); + }); + + test('list and change database tests for invalid uris', async () => { + let badString = 'bad_string'; + let listDatabasesResult = await connectionManagementService.listDatabases(badString); + assert(!listDatabasesResult); + let changeDatabaseResult = await connectionManagementService.changeDatabase(badString, badString); + assert(!changeDatabaseResult); + }); test('getTabColorForUri returns undefined when there is no connection for the given URI', () => { @@ -1193,7 +1521,7 @@ suite('SQL ConnectionManagementService tests', () => { assert.equal(color, undefined); }); - test('getTabColorForUri returns the group color corresponding to the connection for a URI', () => { + test('getTabColorForUri returns the group color corresponding to the connection for a URI', async () => { // Set up the connection store to give back a group for the expected connection profile configResult['tabColorMode'] = 'border'; let expectedColor = 'red'; @@ -1201,10 +1529,9 @@ suite('SQL ConnectionManagementService tests', () => { color: expectedColor }); let uri = 'testUri'; - return connect(uri).then(() => { - let tabColor = connectionManagementService.getTabColorForUri(uri); - assert.equal(tabColor, expectedColor); - }); + await connect(uri); + let tabColor = connectionManagementService.getTabColorForUri(uri); + assert.equal(tabColor, expectedColor); }); test('getConnectionCredentials returns the credentials dictionary for an active connection profile', async () => { @@ -1219,6 +1546,7 @@ suite('SQL ConnectionManagementService tests', () => { test('getConnectionCredentials returns the credentials dictionary for a recently used connection profile', async () => { const test_password = 'test_password'; + let badString = 'bad_string'; const profile = createConnectionProfile('test_id', ''); const connectionStoreMock = TypeMoq.Mock.ofType(ConnectionStore, TypeMoq.MockBehavior.Loose, new TestStorageService()); connectionStoreMock.setup(x => x.getRecentlyUsedConnections(undefined)).returns(() => { @@ -1231,6 +1559,9 @@ suite('SQL ConnectionManagementService tests', () => { const connectionManagementService = new ConnectionManagementService(connectionStoreMock.object, undefined, undefined, undefined, undefined, undefined, undefined, new TestCapabilitiesService(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, getBasicExtensionService()); assert.equal(profile.password, '', 'Profile should not have password initially'); assert.equal(profile.options['password'], '', 'Profile options should not have password initially'); + // Check for invalid profile id + let badCredentials = await connectionManagementService.getConnectionCredentials(badString); + assert.equal(badCredentials, undefined); let credentials = await connectionManagementService.getConnectionCredentials(profile.id); assert.equal(credentials['password'], test_password); }); @@ -1474,7 +1805,7 @@ test('clearRecentConnection and ConnectionsList should call connectionStore func assert(called); }); -function createConnectionProfile(id: string, password?: string): ConnectionProfile { +export function createConnectionProfile(id: string, password?: string): ConnectionProfile { const capabilitiesService = new TestCapabilitiesService(); return new ConnectionProfile(capabilitiesService, { connectionName: 'newName', diff --git a/src/sql/workbench/services/connection/test/browser/media/views.css b/src/sql/workbench/services/connection/test/browser/media/views.css new file mode 100644 index 0000000000..6adfc52db4 --- /dev/null +++ b/src/sql/workbench/services/connection/test/browser/media/views.css @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* File icons in trees */ + +.file-icon-themable-tree.align-icons-and-twisties .monaco-tl-twistie:not(.force-twistie):not(.collapsible), +.file-icon-themable-tree .align-icon-with-twisty .monaco-tl-twistie:not(.force-twistie):not(.collapsible), +.file-icon-themable-tree.hide-arrows .monaco-tl-twistie:not(.force-twistie), +.file-icon-themable-tree .monaco-tl-twistie.force-no-twistie { + background-image: none !important; + width: 0 !important; + padding-right: 0 !important; + visibility: hidden; +} + +/* Misc */ + +.monaco-workbench .tree-explorer-viewlet-tree-view { + height: 100%; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .message { + display: flex; + padding: 4px 12px 4px 18px; + user-select: text; + -webkit-user-select: text; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .message p { + margin-top: 0px; + margin-bottom: 0px; + padding-bottom: 4px; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .message ul { + padding-left: 24px; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .message.hide { + display: none; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .customview-tree { + height: 100%; +} + +.monaco-workbench .tree-explorer-viewlet-tree-view .customview-tree.hide { + display: none; +} + +.monaco-workbench .pane > .pane-body > .welcome-view { + width: 100%; + height: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.monaco-workbench .pane > .pane-body:not(.welcome) > .welcome-view, +.monaco-workbench .pane > .pane-body.welcome > :not(.welcome-view) { + display: none; +} + +.monaco-workbench .pane > .pane-body > .welcome-view .monaco-button { + max-width: 260px; + margin-left: auto; + margin-right: auto; +} + +.monaco-workbench .pane > .pane-body .welcome-view-content { + padding: 0 20px 0 20px; + box-sizing: border-box; +} + +.monaco-workbench .pane > .pane-body .welcome-view-content > * { + margin-block-start: 1em; + margin-block-end: 1em; + margin-inline-start: 0px; + margin-inline-end: 0px; +} + +.customview-tree .monaco-list-row .monaco-tl-contents.align-icon-with-twisty::before { + display: none; +} + +.customview-tree .monaco-list-row .monaco-tl-contents:not(.align-icon-with-twisty)::before { + display: inline-block; +} + +.customview-tree .monaco-list .monaco-list-row { + padding-right: 12px; + padding-left: 0px; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item { + display: flex; + height: 22px; + line-height: 22px; + flex: 1; + text-overflow: ellipsis; + overflow: hidden; + flex-wrap: nowrap; + padding-left: 3px; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-inputbox { + line-height: normal; + flex: 1; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel { + flex: 1; + text-overflow: ellipsis; + overflow: hidden; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon { + background-size: 16px; + background-position: left center; + background-repeat: no-repeat; + padding-right: 6px; + width: 16px; + height: 22px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { + margin-top: 3px; +} + +.customview-tree .monaco-list .monaco-list-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-icon.codicon { + color: currentColor !important; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel .monaco-icon-label-container > .monaco-icon-name-container { + flex: 1; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel::after { + padding-right: 0px; +} + +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .actions { + display: none; +} + +.customview-tree .monaco-list .monaco-list-row:hover .custom-view-tree-node-item .actions, +.customview-tree .monaco-list .monaco-list-row.selected .custom-view-tree-node-item .actions, +.customview-tree .monaco-list .monaco-list-row.focused .custom-view-tree-node-item .actions { + display: block; +} + +.customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label { + width: 16px; + height: 100%; + background-size: 16px; + background-position: 50% 50%; + background-repeat: no-repeat; +} + +.customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label.codicon { + line-height: 22px; +} + +.customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label.codicon::before { + vertical-align: middle; +} diff --git a/src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts b/src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts new file mode 100644 index 0000000000..7c0cdb7fc1 --- /dev/null +++ b/src/sql/workbench/services/connection/test/browser/testConnectionDialogWidget.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionDialogWidget } from 'sql/workbench/services/connection/browser/connectionDialogWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +export class TestConnectionDialogWidget extends ConnectionDialogWidget { + constructor( + providerDisplayNameOptions: string[], + selectedProviderType: string, + providerNameToDisplayNameMap: { [providerDisplayName: string]: string }, + @IInstantiationService _instantiationService: IInstantiationService, + @IConnectionManagementService _connectionManagementService: IConnectionManagementService, + @IContextMenuService _contextMenuService: IContextMenuService, + @IContextViewService _contextViewService: IContextViewService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IThemeService themeService: IThemeService, + @ILayoutService layoutService: ILayoutService, + @IAdsTelemetryService telemetryService: IAdsTelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService, + @ILogService logService: ILogService, + @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(providerDisplayNameOptions, selectedProviderType, providerNameToDisplayNameMap, _instantiationService, _connectionManagementService, _contextMenuService, _contextViewService, viewDescriptorService, themeService, layoutService, telemetryService, contextKeyService, clipboardService, logService, textResourcePropertiesService, configurationService); + } + public renderBody(container: HTMLElement) { + super.renderBody(container); + } +} diff --git a/src/sql/workbench/services/connection/test/browser/testTreeView.ts b/src/sql/workbench/services/connection/test/browser/testTreeView.ts new file mode 100644 index 0000000000..ed74d632fd --- /dev/null +++ b/src/sql/workbench/services/connection/test/browser/testTreeView.ts @@ -0,0 +1,1049 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/views'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import * as DOM from 'vs/base/browser/dom'; +import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { URI } from 'vs/base/common/uri'; +import { dirname, basename } from 'vs/base/common/resources'; +import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService'; +import { FileKind } from 'vs/platform/files/common/files'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { localize } from 'vs/nls'; +import { timeout } from 'vs/base/common/async'; +import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; +import { isString } from 'vs/base/common/types'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; +import { IHoverService, IHoverOptions, IHoverTarget } from 'vs/workbench/services/hover/browser/hover'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { isMacintosh } from 'vs/base/common/platform'; + +class Root implements ITreeItem { + label = { label: 'root' }; + handle = '0'; + parentHandle: string | undefined = undefined; + collapsibleState = TreeItemCollapsibleState.Expanded; + children: ITreeItem[] | undefined = undefined; +} + +const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); + +class Tree extends WorkbenchAsyncDataTree { } + +export class TreeView extends Disposable implements ITreeView { + + private isVisible: boolean = false; + private _hasIconForParentNode = false; + private _hasIconForLeafNode = false; + + private readonly collapseAllContextKey: RawContextKey; + private readonly collapseAllContext: IContextKey; + private readonly refreshContextKey: RawContextKey; + private readonly refreshContext: IContextKey; + + private focused: boolean = false; + private domNode!: HTMLElement; + private treeContainer!: HTMLElement; + private _messageValue: string | undefined; + private _canSelectMany: boolean = false; + private messageElement!: HTMLDivElement; + private tree: Tree | undefined; + private treeLabels: ResourceLabels | undefined; + + public root: ITreeItem; // {{SQL CARBON EDIT}} + private elementsToRefresh: ITreeItem[] = []; + + private readonly _onDidExpandItem: Emitter = this._register(new Emitter()); + readonly onDidExpandItem: Event = this._onDidExpandItem.event; + + private readonly _onDidCollapseItem: Emitter = this._register(new Emitter()); + readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; + + private _onDidChangeSelection: Emitter = this._register(new Emitter()); + readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + + private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + + private readonly _onDidChangeActions: Emitter = this._register(new Emitter()); + readonly onDidChangeActions: Event = this._onDidChangeActions.event; + + private readonly _onDidChangeWelcomeState: Emitter = this._register(new Emitter()); + readonly onDidChangeWelcomeState: Event = this._onDidChangeWelcomeState.event; + + private readonly _onDidChangeTitle: Emitter = this._register(new Emitter()); + readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; + + private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); + + constructor( + readonly id: string, + private _title: string, + @IThemeService private readonly themeService: IThemeService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProgressService protected readonly progressService: IProgressService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @INotificationService private readonly notificationService: INotificationService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(); + this.root = new Root(); + this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, false); + this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService); + this.refreshContextKey = new RawContextKey(`treeView.${this.id}.enableRefresh`, false); + this.refreshContext = this.refreshContextKey.bindTo(contextKeyService); + + this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); + this._register(this.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('explorer.decorations')) { + this.doRefresh([this.root]).catch(err => { }); /** soft refresh **/ + } + })); + this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { + if (views.some(v => v.id === this.id)) { + this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }); + } + })); + this.registerActions(); + + this.create(); + } + + get viewContainer(): ViewContainer { + return this.viewDescriptorService.getViewContainerByViewId(this.id)!; + } + + get viewLocation(): ViewContainerLocation { + return this.viewDescriptorService.getViewLocationById(this.id)!; + } + + private _dataProvider: ITreeViewDataProvider | undefined; + get dataProvider(): ITreeViewDataProvider | undefined { + return this._dataProvider; + } + + set dataProvider(dataProvider: ITreeViewDataProvider | undefined) { + if (this.tree === undefined) { + this.createTree(); + } + + if (dataProvider) { + this._dataProvider = new class implements ITreeViewDataProvider { + private _isEmpty: boolean = true; + private _onDidChangeEmpty: Emitter = new Emitter(); + public onDidChangeEmpty: Event = this._onDidChangeEmpty.event; + + get isTreeEmpty(): boolean { + return this._isEmpty; + } + + async getChildren(node: ITreeItem): Promise { + let children: ITreeItem[]; + if (node && node.children) { + children = node.children; + } else { + children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); + node.children = children; + } + if (node instanceof Root) { + const oldEmpty = this._isEmpty; + this._isEmpty = children.length === 0; + if (oldEmpty !== this._isEmpty) { + this._onDidChangeEmpty.fire(); + } + } + return children; + } + }; + if (this._dataProvider.onDidChangeEmpty) { + this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire())); + } + this.updateMessage(); + this.refresh().catch(err => { }); + } else { + this._dataProvider = undefined; + this.updateMessage(); + } + + this._onDidChangeWelcomeState.fire(); + } + + private _message: string | undefined; + get message(): string | undefined { + return this._message; + } + + set message(message: string | undefined) { + this._message = message; + this.updateMessage(); + this._onDidChangeWelcomeState.fire(); + } + + get title(): string { + return this._title; + } + + set title(name: string) { + this._title = name; + this._onDidChangeTitle.fire(this._title); + } + + get canSelectMany(): boolean { + return this._canSelectMany; + } + + set canSelectMany(canSelectMany: boolean) { + this._canSelectMany = canSelectMany; + } + + get hasIconForParentNode(): boolean { + return this._hasIconForParentNode; + } + + get hasIconForLeafNode(): boolean { + return this._hasIconForLeafNode; + } + + get visible(): boolean { + return this.isVisible; + } + + get showCollapseAllAction(): boolean { + return !!this.collapseAllContext.get(); + } + + set showCollapseAllAction(showCollapseAllAction: boolean) { + this.collapseAllContext.set(showCollapseAllAction); + } + + get showRefreshAction(): boolean { + return !!this.refreshContext.get(); + } + + set showRefreshAction(showRefreshAction: boolean) { + this.refreshContext.set(showRefreshAction); + } + + private registerActions() { + const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.refresh`, + title: localize('refresh', "Refresh"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey), + group: 'navigation', + order: Number.MAX_SAFE_INTEGER - 1, + }, + icon: { id: 'codicon/refresh' } + }); + } + async run(): Promise { + return that.refresh(); + } + })); + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.collapseAll`, + title: localize('collapseAll', "Collapse All"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey), + group: 'navigation', + order: Number.MAX_SAFE_INTEGER, + }, + icon: { id: 'codicon/collapse-all' } + }); + } + async run(): Promise { + if (that.tree) { + return new CollapseAllAction(that.tree, true).run(); + } + } + })); + } + + setVisibility(isVisible: boolean): void { + isVisible = !!isVisible; + if (this.isVisible === isVisible) { + return; + } + + this.isVisible = isVisible; + + if (this.tree) { + if (this.isVisible) { + DOM.show(this.tree.getHTMLElement()); + } else { + DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it + } + + if (this.isVisible && this.elementsToRefresh.length) { + this.doRefresh(this.elementsToRefresh).catch(err => { }); + this.elementsToRefresh = []; + } + } + + this._onDidChangeVisibility.fire(this.isVisible); + } + + focus(reveal: boolean = true): void { + if (this.tree && this.root.children && this.root.children.length > 0) { + // Make sure the current selected element is revealed + const selectedElement = this.tree.getSelection()[0]; + if (selectedElement && reveal) { + this.tree.reveal(selectedElement, 0.5); + } + + // Pass Focus to Viewer + this.tree.domFocus(); + } else if (this.tree) { + this.tree.domFocus(); + } else { + this.domNode.focus(); + } + } + + show(container: HTMLElement): void { + DOM.append(container, this.domNode); + } + + private create() { + this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); + this.messageElement = DOM.append(this.domNode, DOM.$('.message')); + this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); + this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); + const focusTracker = this._register(DOM.trackFocus(this.domNode)); + this._register(focusTracker.onDidFocus(() => this.focused = true)); + this._register(focusTracker.onDidBlur(() => this.focused = false)); + } + + private createTree() { + const actionViewItemProvider = (action: IAction) => { + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(MenuEntryActionViewItem, action); + } else if (action instanceof SubmenuItemAction) { + return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); + } + + return undefined; + }; + const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); + this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); + const aligner = new Aligner(this.themeService); + const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); + const widgetAriaLabel = this._title; + + this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer], + dataSource, { + identityProvider: new TreeViewIdentityProvider(), + accessibilityProvider: { + getAriaLabel(element: ITreeItem): string { + if (element.accessibilityInformation) { + return element.accessibilityInformation.label; + } + + return isString(element.tooltip) ? element.tooltip : element.label ? element.label.label : ''; + }, + getRole(element: ITreeItem): string | undefined { + return element.accessibilityInformation?.role ?? 'treeitem'; + }, + getWidgetAriaLabel(): string { + return widgetAriaLabel; + } + }, + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (item: ITreeItem) => { + return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); + } + }, + expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command, + collapseByDefault: (e: ITreeItem): boolean => { + return e.collapsibleState !== TreeItemCollapsibleState.Expanded; + }, + multipleSelectionSupport: this.canSelectMany, + overrideStyles: { + listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND + } + }) as WorkbenchAsyncDataTree); + aligner.tree = this.tree; + const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()); + renderer.actionRunner = actionRunner; + + this.tree.contextKeyService.createKey(this.id, true); + this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); + this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this._register(this.tree.onDidChangeCollapseState(e => { + if (!e.node.element) { + return; + } + + const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; + if (e.node.collapsed) { + this._onDidCollapseItem.fire(element); + } else { + this._onDidExpandItem.fire(element); + } + })); + this.tree.setInput(this.root).then(() => this.updateContentAreas()); + + this._register(this.tree.onDidOpen(e => { + if (!e.browserEvent) { + return; + } + const selection = this.tree!.getSelection(); + if ((selection.length === 1) && selection[0].command) { + this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || [])); + } + })); + + } + + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { + const node: ITreeItem | null = treeEvent.element; + if (node === null) { + return; + } + const event: UIEvent = treeEvent.browserEvent; + + event.preventDefault(); + event.stopPropagation(); + + this.tree!.setFocus([node]); + const actions = treeMenus.getResourceContextActions(node); + if (!actions.length) { + return; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + + getActions: () => actions, + + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.tree!.domFocus(); + } + }, + + getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), + + actionRunner + }); + } + + protected updateMessage(): void { + if (this._message) { + this.showMessage(this._message); + } else if (!this.dataProvider) { + this.showMessage(noDataProviderMessage); + } else { + this.hideMessage(); + } + this.updateContentAreas(); + } + + private showMessage(message: string): void { + this.messageElement.classList.remove('hide'); + this.resetMessageElement(); + this._messageValue = message; + if (!isFalsyOrWhitespace(this._message)) { + this.messageElement.textContent = this._messageValue; + } + this.layout(this._height, this._width); + } + + private hideMessage(): void { + this.resetMessageElement(); + this.messageElement.classList.add('hide'); + this.layout(this._height, this._width); + } + + private resetMessageElement(): void { + DOM.clearNode(this.messageElement); + } + + private _height: number = 0; + private _width: number = 0; + layout(height: number, width: number) { + if (height && width) { + this._height = height; + this._width = width; + const treeHeight = height - DOM.getTotalHeight(this.messageElement); + this.treeContainer.style.height = treeHeight + 'px'; + if (this.tree) { + this.tree.layout(treeHeight, width); + } + } + } + + getOptimalWidth(): number { + if (this.tree) { + const parentNode = this.tree.getHTMLElement(); + const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a')); + return DOM.getLargestChildWidth(parentNode, childNodes); + } + return 0; + } + + async refresh(elements?: ITreeItem[]): Promise { + if (this.dataProvider && this.tree) { + if (this.refreshing) { + await Event.toPromise(this._onDidCompleteRefresh.event); + } + if (!elements) { + elements = [this.root]; + // remove all waiting elements to refresh if root is asked to refresh + this.elementsToRefresh = []; + } + for (const element of elements) { + element.children = undefined; // reset children + } + if (this.isVisible) { + return this.doRefresh(elements); + } else { + if (this.elementsToRefresh.length) { + const seen: Set = new Set(); + this.elementsToRefresh.forEach(element => seen.add(element.handle)); + for (const element of elements) { + if (!seen.has(element.handle)) { + this.elementsToRefresh.push(element); + } + } + } else { + this.elementsToRefresh.push(...elements); + } + } + } + return undefined; + } + + async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { + const tree = this.tree; + if (tree) { + itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; + await Promise.all(itemOrItems.map(element => { + return tree.expand(element, false); + })); + } + } + + setSelection(items: ITreeItem[]): void { + if (this.tree) { + this.tree.setSelection(items); + } + } + + setFocus(item: ITreeItem): void { + if (this.tree) { + this.focus(); + this.tree.setFocus([item]); + } + } + + async reveal(item: ITreeItem): Promise { + if (this.tree) { + return this.tree.reveal(item); + } + } + + private refreshing: boolean = false; + private async doRefresh(elements: ITreeItem[]): Promise { + const tree = this.tree; + if (tree && this.visible) { + this.refreshing = true; + await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); + this.refreshing = false; + this._onDidCompleteRefresh.fire(); + this.updateContentAreas(); + if (this.focused) { + this.focus(false); + } + } + } + + private updateContentAreas(): void { + const isTreeEmpty = !this.root.children || this.root.children.length === 0; + // Hide tree container only when there is a message and tree is empty and not refreshing + if (this._messageValue && isTreeEmpty && !this.refreshing) { + this.treeContainer.classList.add('hide'); + this.domNode.setAttribute('tabindex', '0'); + } else { + this.treeContainer.classList.remove('hide'); + this.domNode.removeAttribute('tabindex'); + } + } +} + +class TreeViewIdentityProvider implements IIdentityProvider { + getId(element: ITreeItem): { toString(): string; } { + return element.handle; + } +} + +class TreeViewDelegate implements IListVirtualDelegate { + + getHeight(element: ITreeItem): number { + return TreeRenderer.ITEM_HEIGHT; + } + + getTemplateId(element: ITreeItem): string { + return TreeRenderer.TREE_TEMPLATE_ID; + } +} + +class TreeDataSource implements IAsyncDataSource { + + constructor( + private treeView: ITreeView, + private withProgress: (task: Promise) => Promise + ) { + } + + hasChildren(element: ITreeItem): boolean { + return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None); + } + + async getChildren(element: ITreeItem): Promise { + if (this.treeView.dataProvider) { + return this.withProgress(this.treeView.dataProvider.getChildren(element)); + } + return []; + } +} + +// todo@joh,sandy make this proper and contributable from extensions +registerThemingParticipant((theme, collector) => { + + const matchBackgroundColor = theme.getColor(listFilterMatchHighlight); + if (matchBackgroundColor) { + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); + collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); + } + const matchBorderColor = theme.getColor(listFilterMatchHighlightBorder); + if (matchBorderColor) { + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); + collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); + } + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`); + } + const focusBorderColor = theme.getColor(focusBorder); + if (focusBorderColor) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focusBorderColor}; outline-offset: -1px; }`); + } + const codeBackground = theme.getColor(textCodeBlockBackground); + if (codeBackground) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`); + } +}); + +interface ITreeExplorerTemplateData { + elementDisposable: IDisposable; + container: HTMLElement; + resourceLabel: IResourceLabel; + icon: HTMLElement; + actionBar: ActionBar; +} + +class TreeRenderer extends Disposable implements ITreeRenderer { + static readonly ITEM_HEIGHT = 22; + static readonly TREE_TEMPLATE_ID = 'treeExplorer'; + + private _actionRunner: MultipleSelectionActionRunner | undefined; + + constructor( + private treeViewId: string, + private menus: TreeMenus, + private labels: ResourceLabels, + private actionViewItemProvider: IActionViewItemProvider, + private aligner: Aligner, + @IThemeService private readonly themeService: IThemeService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILabelService private readonly labelService: ILabelService, + @IHoverService private readonly hoverService: IHoverService + ) { + super(); + } + + get templateId(): string { + return TreeRenderer.TREE_TEMPLATE_ID; + } + + set actionRunner(actionRunner: MultipleSelectionActionRunner) { + this._actionRunner = actionRunner; + } + + renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { + container.classList.add('custom-view-tree-node-item'); + + const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); + + const resourceLabel = this.labels.create(container, { supportHighlights: true }); + const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionViewItemProvider: this.actionViewItemProvider + }); + + return { resourceLabel, icon, actionBar, container, elementDisposable: Disposable.None }; + } + + renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + const node = element.element; + const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; + const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : (resource ? { label: basename(resource) } : undefined); + const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined; + const label = treeItemLabel ? treeItemLabel.label : undefined; + const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => { + if (start < 0) { + start = label.length + start; + } + if (end < 0) { + end = label.length + end; + } + if ((start >= label.length) || (end > label.length)) { + return ({ start: 0, end: 0 }); + } + if (start > end) { + const swap = start; + start = end; + end = swap; + } + return ({ start, end }); + }) : undefined; + const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark; + const iconUrl = icon ? URI.revive(icon) : null; + + // reset + templateData.actionBar.clear(); + let fallbackHover = label; + if (resource || this.isFileKindThemeIcon(node.themeIcon)) { + const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); + const labelResource = resource ? resource : URI.parse('missing:_icon_resource'); + templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, { + fileKind: this.getFileKind(node), + title: '', + hideIcon: !!iconUrl, + fileDecorations, + extraClasses: ['custom-view-tree-node-item-resourceLabel'], + matches: matches ? matches : createMatches(element.filterData), + strikethrough: treeItemLabel?.strikethrough + }); + fallbackHover = this.labelService.getUriLabel(labelResource); + } else { + templateData.resourceLabel.setResource({ name: label, description }, { + title: '', + hideIcon: true, + extraClasses: ['custom-view-tree-node-item-resourceLabel'], + matches: matches ? matches : createMatches(element.filterData), + strikethrough: treeItemLabel?.strikethrough + }); + } + + if (iconUrl) { + templateData.icon.className = 'custom-view-tree-node-item-icon'; + templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); + } else { + let iconClass: string | undefined; + if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) { + iconClass = ThemeIcon.asClassName(node.themeIcon); + } + templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : ''; + templateData.icon.style.backgroundImage = ''; + } + templateData.icon.title = ''; + + templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; + templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); + if (this._actionRunner) { + templateData.actionBar.actionRunner = this._actionRunner; + } + this.setAlignment(templateData.container, node); + const disposableStore = new DisposableStore(); + templateData.elementDisposable = disposableStore; + disposableStore.add(this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); + this.setupHovers(node, templateData.resourceLabel.element.firstElementChild!, disposableStore, fallbackHover); + this.setupHovers(node, templateData.icon, disposableStore, fallbackHover); + } + + private setupHovers(node: ITreeItem, htmlElement: HTMLElement, disposableStore: DisposableStore, label: string | undefined): void { + const hoverService = this.hoverService; + // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. + // On Mac, the delay is 1500. + const hoverDelay = isMacintosh ? 1500 : 500; + let hoverOptions: IHoverOptions | undefined; + let mouseX: number | undefined; + function mouseOver(this: HTMLElement, e: MouseEvent): any { + let isHovering = true; + function mouseMove(this: HTMLElement, e: MouseEvent): any { + mouseX = e.x; + } + function mouseLeave(this: HTMLElement, e: MouseEvent): any { + isHovering = false; + } + this.addEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave, { passive: true }); + this.addEventListener(DOM.EventType.MOUSE_MOVE, mouseMove, { passive: true }); + setTimeout(async () => { + if (node instanceof ResolvableTreeItem) { + await node.resolve(); + } + let tooltip: IMarkdownString | string | undefined = node.tooltip ?? label; + if (isHovering && tooltip) { + if (!hoverOptions) { + const target: IHoverTarget = { + targetElements: [this], + dispose: () => { } + }; + hoverOptions = { text: tooltip, target }; + } + if (mouseX !== undefined) { + (hoverOptions.target).x = mouseX; + } + hoverService.showHover(hoverOptions); + } + this.removeEventListener(DOM.EventType.MOUSE_MOVE, mouseMove); + this.removeEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave); + }, hoverDelay); + } + htmlElement.addEventListener(DOM.EventType.MOUSE_OVER, mouseOver, { passive: true }); + disposableStore.add({ + dispose: () => { + htmlElement.removeEventListener(DOM.EventType.MOUSE_OVER, mouseOver); + } + }); + } + + private setAlignment(container: HTMLElement, treeItem: ITreeItem) { + DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); + } + + private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean { + if (icon) { + return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id; + } else { + return false; + } + } + + private getFileKind(node: ITreeItem): FileKind { + if (node.themeIcon) { + switch (node.themeIcon.id) { + case FileThemeIcon.id: + return FileKind.FILE; + case FolderThemeIcon.id: + return FileKind.FOLDER; + } + } + return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; + } + + disposeElement(resource: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + } + + disposeTemplate(templateData: ITreeExplorerTemplateData): void { + templateData.resourceLabel.dispose(); + templateData.actionBar.dispose(); + templateData.elementDisposable.dispose(); + } +} + +class Aligner extends Disposable { + private _tree: WorkbenchAsyncDataTree | undefined; + + constructor(private themeService: IThemeService) { + super(); + } + + set tree(tree: WorkbenchAsyncDataTree) { + this._tree = tree; + } + + public alignIconWithTwisty(treeItem: ITreeItem): boolean { + if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) { + return false; + } + if (!this.hasIcon(treeItem)) { + return false; + } + + if (this._tree) { + const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); + if (this.hasIcon(parent)) { + return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIcon(c)); + } + return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); + } else { + return false; + } + } + + private hasIcon(node: ITreeItem): boolean { + const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark; + if (icon) { + return true; + } + if (node.resourceUri || node.themeIcon) { + const fileIconTheme = this.themeService.getFileIconTheme(); + const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None; + if (isFolder) { + return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons; + } + return fileIconTheme.hasFileIcons; + } + return false; + } +} + +class MultipleSelectionActionRunner extends ActionRunner { + + constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) { + super(); + this._register(this.onDidRun(e => { + if (e.error) { + notificationService.error(localize('command-error', 'Error running command {1}: {0}. This is likely caused by the extension that contributes {1}.', e.error.message, e.action.id)); + } + })); + } + + runAction(action: IAction, context: TreeViewItemHandleArg): Promise { + const selection = this.getSelectedResources(); + let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined; + let actionInSelected: boolean = false; + if (selection.length > 1) { + selectionHandleArgs = selection.map(selected => { + if (selected.handle === context.$treeItemHandle) { + actionInSelected = true; + } + return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle }; + }); + } + + if (!actionInSelected) { + selectionHandleArgs = undefined; + } + + return action.run(...[context, selectionHandleArgs]); + } +} + +class TreeMenus extends Disposable implements IDisposable { + + constructor( + private id: string, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService + ) { + super(); + } + + getResourceActions(element: ITreeItem): IAction[] { + return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).primary; + } + + getResourceContextActions(element: ITreeItem): IAction[] { + return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary; + } + + private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } { + const contextKeyService = this.contextKeyService.createScoped(); + contextKeyService.createKey('view', this.id); + contextKeyService.createKey(context.key, context.value); + + const menu = this.menuService.createMenu(menuId, contextKeyService); + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + + menu.dispose(); + + return result; + } +} + +export class TestTreeView extends TreeView { + + private activated: boolean = false; + + constructor( + id: string, + title: string, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @ICommandService commandService: ICommandService, + @IConfigurationService configurationService: IConfigurationService, + @IProgressService progressService: IProgressService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, contextKeyService); + } + + setVisibility(isVisible: boolean): void { + super.setVisibility(isVisible); + if (this.visible) { + this.activate(); + } + } + + private activate() { + if (!this.activated) { + this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + .then(() => timeout(2000)) + .then(() => { + this.updateMessage(); + }); + this.activated = true; + } + } +}