diff --git a/extensions/schema-compare/src/controllers/mainController.ts b/extensions/schema-compare/src/controllers/mainController.ts index 0557f1eee7..592198afc8 100644 --- a/extensions/schema-compare/src/controllers/mainController.ts +++ b/extensions/schema-compare/src/controllers/mainController.ts @@ -30,7 +30,7 @@ export default class MainController implements Disposable { } private initializeSchemaCompareDialog(): void { - this.apiWrapper.registerCommand('schemaCompare.start', (context: any) => this.schemaCompareMainWindow.start(context)); + this.apiWrapper.registerCommand('schemaCompare.start', async (context: any) => { await this.schemaCompareMainWindow.start(context); }); } public dispose(): void { diff --git a/extensions/schema-compare/src/schemaCompareMainWindow.ts b/extensions/schema-compare/src/schemaCompareMainWindow.ts index c6afcdcaf7..f525dc05ef 100644 --- a/extensions/schema-compare/src/schemaCompareMainWindow.ts +++ b/extensions/schema-compare/src/schemaCompareMainWindow.ts @@ -82,12 +82,12 @@ export class SchemaCompareMainWindow { // 1. undefined // 2. connection profile // 3. dacpac - public async start(context: any) { + public async start(context: any): Promise { // if schema compare was launched from a db, set that as the source let profile = context ? context.connectionProfile : undefined; let sourceDacpac = context as string; if (profile) { - let ownerUri = await azdata.connection.getUriForConnection((profile.id)); + let ownerUri = await this.apiWrapper.getUriForConnection((profile.id)); this.sourceEndpointInfo = { endpointType: mssql.SchemaCompareEndpointType.Database, serverDisplayName: `${profile.serverName} ${profile.userName}`, @@ -109,138 +109,146 @@ export class SchemaCompareMainWindow { }; } - this.editor.registerContent(async view => { - this.differencesTable = view.modelBuilder.table().withProperties({ - data: [], - title: loc.differencesTableTitle - }).component(); - - this.diffEditor = view.modelBuilder.diffeditor().withProperties({ - contentLeft: os.EOL, - contentRight: os.EOL, - height: 500, - title: loc.diffEditorTitle - }).component(); - - this.splitView = view.modelBuilder.splitViewContainer().component(); - - let sourceTargetLabels = view.modelBuilder.flexContainer() - .withProperties({ - alignItems: 'stretch', - horizontal: true - }).component(); - - this.sourceTargetFlexLayout = view.modelBuilder.flexContainer() - .withProperties({ - alignItems: 'stretch', - horizontal: true - }).component(); - - this.createSwitchButton(view); - this.createCompareButton(view); - this.createCancelButton(view); - this.createGenerateScriptButton(view); - this.createApplyButton(view); - this.createOptionsButton(view); - this.createOpenScmpButton(view); - this.createSaveScmpButton(view); - this.createSourceAndTargetButtons(view); - this.resetButtons(ResetButtonState.noSourceTarget); - - let toolBar = view.modelBuilder.toolbarContainer(); - toolBar.addToolbarItems([{ - component: this.compareButton - }, { - component: this.cancelCompareButton - }, { - component: this.generateScriptButton - }, { - component: this.applyButton - }, { - component: this.optionsButton, - toolbarSeparatorAfter: true - }, { - component: this.switchButton, - toolbarSeparatorAfter: true - }, { - component: this.openScmpButton - }, { - component: this.saveScmpButton - }]); - - let sourceLabel = view.modelBuilder.text().withProperties({ - value: loc.sourceTitle, - CSSStyles: { 'margin-bottom': '0px' } - }).component(); - - let targetLabel = view.modelBuilder.text().withProperties({ - value: loc.targetTitle, - CSSStyles: { 'margin-bottom': '0px' } - }).component(); - - let arrowLabel = view.modelBuilder.text().withProperties({ - value: '➔' - }).component(); - - this.sourceName = getEndpointName(this.sourceEndpointInfo); - this.targetName = ' '; - this.sourceNameComponent = view.modelBuilder.table().withProperties({ - columns: [ - { - value: this.sourceName, - headerCssClass: 'no-borders', - toolTip: this.sourceName - }, - ] - }).component(); - - this.targetNameComponent = view.modelBuilder.table().withProperties({ - columns: [ - { - value: this.targetName, - headerCssClass: 'no-borders', - toolTip: this.targetName - }, - ] - }).component(); - - sourceTargetLabels.addItem(sourceLabel, { CSSStyles: { 'width': '55%', 'margin-left': '15px', 'font-size': 'larger', 'font-weight': 'bold' } }); - sourceTargetLabels.addItem(targetLabel, { CSSStyles: { 'width': '45%', 'font-size': 'larger', 'font-weight': 'bold' } }); - this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } }); - this.sourceTargetFlexLayout.addItem(this.selectSourceButton, { CSSStyles: { 'margin-top': '10px' } }); - this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } }); - this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } }); - this.sourceTargetFlexLayout.addItem(this.selectTargetButton, { CSSStyles: { 'margin-top': '10px' } }); - - this.loader = view.modelBuilder.loadingComponent().component(); - this.waitText = view.modelBuilder.text().withProperties({ - value: loc.waitText - }).component(); - - this.startText = view.modelBuilder.text().withProperties({ - value: loc.startText - }).component(); - - this.noDifferencesLabel = view.modelBuilder.text().withProperties({ - value: loc.noDifferencesText - }).component(); - - this.flexModel = view.modelBuilder.flexContainer().component(); - this.flexModel.addItem(toolBar.component(), { flex: 'none' }); - this.flexModel.addItem(sourceTargetLabels, { flex: 'none' }); - this.flexModel.addItem(this.sourceTargetFlexLayout, { flex: 'none' }); - this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } }); - - this.flexModel.setLayout({ - flexFlow: 'column', - height: '100%' - }); - - await view.initializeModel(this.flexModel); - }); - await this.GetDefaultDeploymentOptions(); - this.editor.openEditor(); + await Promise.all([ + this.registerContent(), + this.editor.openEditor() + ]); + } + + private async registerContent(): Promise { + return new Promise((resolve) => { + this.editor.registerContent(async view => { + this.differencesTable = view.modelBuilder.table().withProperties({ + data: [], + title: loc.differencesTableTitle + }).component(); + + this.diffEditor = view.modelBuilder.diffeditor().withProperties({ + contentLeft: os.EOL, + contentRight: os.EOL, + height: 500, + title: loc.diffEditorTitle + }).component(); + + this.splitView = view.modelBuilder.splitViewContainer().component(); + + let sourceTargetLabels = view.modelBuilder.flexContainer() + .withProperties({ + alignItems: 'stretch', + horizontal: true + }).component(); + + this.sourceTargetFlexLayout = view.modelBuilder.flexContainer() + .withProperties({ + alignItems: 'stretch', + horizontal: true + }).component(); + + this.createSwitchButton(view); + this.createCompareButton(view); + this.createCancelButton(view); + this.createGenerateScriptButton(view); + this.createApplyButton(view); + this.createOptionsButton(view); + this.createOpenScmpButton(view); + this.createSaveScmpButton(view); + this.createSourceAndTargetButtons(view); + this.resetButtons(ResetButtonState.noSourceTarget); + + let toolBar = view.modelBuilder.toolbarContainer(); + toolBar.addToolbarItems([{ + component: this.compareButton + }, { + component: this.cancelCompareButton + }, { + component: this.generateScriptButton + }, { + component: this.applyButton + }, { + component: this.optionsButton, + toolbarSeparatorAfter: true + }, { + component: this.switchButton, + toolbarSeparatorAfter: true + }, { + component: this.openScmpButton + }, { + component: this.saveScmpButton + }]); + + let sourceLabel = view.modelBuilder.text().withProperties({ + value: loc.sourceTitle, + CSSStyles: { 'margin-bottom': '0px' } + }).component(); + + let targetLabel = view.modelBuilder.text().withProperties({ + value: loc.targetTitle, + CSSStyles: { 'margin-bottom': '0px' } + }).component(); + + let arrowLabel = view.modelBuilder.text().withProperties({ + value: '➔' + }).component(); + + this.sourceName = getEndpointName(this.sourceEndpointInfo); + this.targetName = ' '; + this.sourceNameComponent = view.modelBuilder.table().withProperties({ + columns: [ + { + value: this.sourceName, + headerCssClass: 'no-borders', + toolTip: this.sourceName + }, + ] + }).component(); + + this.targetNameComponent = view.modelBuilder.table().withProperties({ + columns: [ + { + value: this.targetName, + headerCssClass: 'no-borders', + toolTip: this.targetName + }, + ] + }).component(); + + sourceTargetLabels.addItem(sourceLabel, { CSSStyles: { 'width': '55%', 'margin-left': '15px', 'font-size': 'larger', 'font-weight': 'bold' } }); + sourceTargetLabels.addItem(targetLabel, { CSSStyles: { 'width': '45%', 'font-size': 'larger', 'font-weight': 'bold' } }); + this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } }); + this.sourceTargetFlexLayout.addItem(this.selectSourceButton, { CSSStyles: { 'margin-top': '10px' } }); + this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } }); + this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } }); + this.sourceTargetFlexLayout.addItem(this.selectTargetButton, { CSSStyles: { 'margin-top': '10px' } }); + + this.loader = view.modelBuilder.loadingComponent().component(); + this.waitText = view.modelBuilder.text().withProperties({ + value: loc.waitText + }).component(); + + this.startText = view.modelBuilder.text().withProperties({ + value: loc.startText + }).component(); + + this.noDifferencesLabel = view.modelBuilder.text().withProperties({ + value: loc.noDifferencesText + }).component(); + + this.flexModel = view.modelBuilder.flexContainer().component(); + this.flexModel.addItem(toolBar.component(), { flex: 'none' }); + this.flexModel.addItem(sourceTargetLabels, { flex: 'none' }); + this.flexModel.addItem(this.sourceTargetFlexLayout, { flex: 'none' }); + this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } }); + + this.flexModel.setLayout({ + flexFlow: 'column', + height: '100%' + }); + + await view.initializeModel(this.flexModel); + resolve(); + }); + }); } // update source and target name to display @@ -281,6 +289,15 @@ export class SchemaCompareMainWindow { return this.deploymentOptions; } + // only for test + public verifyButtonsState(generateScriptButtonState: boolean, applyButtonState: boolean): boolean { + let result: boolean = false; + if (this.generateScriptButton.enabled === generateScriptButtonState && this.applyButton.enabled === applyButtonState) { + result = true; + } + return result; + } + public setDeploymentOptions(deploymentOptions: mssql.DeploymentOptions): void { this.deploymentOptions = deploymentOptions; } @@ -298,7 +315,7 @@ export class SchemaCompareMainWindow { .withAdditionalProperties({ operationId: this.comparisonResult.operationId }).send(); - vscode.window.showErrorMessage(loc.compareErrorMessage(this.comparisonResult.errorMessage)); + this.apiWrapper.showErrorMessage(loc.compareErrorMessage(this.comparisonResult.errorMessage)); return; } TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFinished') diff --git a/extensions/schema-compare/src/test/schemaCompare.test.ts b/extensions/schema-compare/src/test/schemaCompare.test.ts index 5b6617b6f1..06c58c00f5 100644 --- a/extensions/schema-compare/src/test/schemaCompare.test.ts +++ b/extensions/schema-compare/src/test/schemaCompare.test.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as should from 'should'; -import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; import * as TypeMoq from 'typemoq'; +import * as loc from '../localizedConstants'; import 'mocha'; import { SchemaCompareDialog } from './../dialogs/schemaCompareDialog'; import { SchemaCompareMainWindow } from '../schemaCompareMainWindow'; -import { SchemaCompareTestService } from './testSchemaCompareService'; +import { SchemaCompareTestService, testStateScmp } from './testSchemaCompareService'; import { createContext, TestContext } from './testContext'; -import { mockIConnectionProfile, mockDacpacEndpoint, mockFilePath } from './testUtils'; +import { mockIConnectionProfile, mockFilePath, setDacpacEndpointInfo, setDatabaseEndpointInfo, shouldThrowSpecificError } from './testUtils'; // Mock test data const mocksource: string = 'source.dacpac'; @@ -26,7 +26,7 @@ before(async function (): Promise { testContext = createContext(); }); describe('SchemaCompareDialog.openDialog', function (): void { - beforeEach(() => { + before(() => { mockExtensionContext = TypeMoq.Mock.ofType(); mockExtensionContext.setup(x => x.extensionPath).returns(() => ''); }); @@ -42,27 +42,22 @@ describe('SchemaCompareDialog.openDialog', function (): void { }); }); -describe('SchemaCompareResult.start', function (): void { - beforeEach(() => { +describe('SchemaCompareMainWindow.start', function (): void { + before(() => { mockExtensionContext = TypeMoq.Mock.ofType(); mockExtensionContext.setup(x => x.extensionPath).returns(() => ''); }); + it('Should be correct when created.', async function (): Promise { let sc = new SchemaCompareTestService(); let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); await result.start(null); - let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized - await promise; should(result.getComparisonResult() === undefined); - let sourceEndpointInfo : mssql.SchemaCompareEndpointInfo = {...mockDacpacEndpoint}; - let targetEndpointInfo : mssql.SchemaCompareEndpointInfo = {...mockDacpacEndpoint}; - result.sourceEndpointInfo = sourceEndpointInfo; - result.sourceEndpointInfo.packageFilePath = mocksource; - result.targetEndpointInfo = targetEndpointInfo; - result.targetEndpointInfo.packageFilePath = mocktarget; + result.sourceEndpointInfo = await setDacpacEndpointInfo(mocksource); + result.targetEndpointInfo = await setDacpacEndpointInfo(mocktarget); await result.execute(); should(result.getComparisonResult() !== undefined); @@ -74,8 +69,6 @@ describe('SchemaCompareResult.start', function (): void { let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); await result.start(undefined); - let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized - await promise; should.equal(result.sourceEndpointInfo, undefined); should.equal(result.targetEndpointInfo, undefined); @@ -86,8 +79,6 @@ describe('SchemaCompareResult.start', function (): void { let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); await result.start({connectionProfile: mockIConnectionProfile}); - let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized - await promise; should.notEqual(result.sourceEndpointInfo, undefined); should.equal(result.sourceEndpointInfo.endpointType, mssql.SchemaCompareEndpointType.Database); @@ -102,8 +93,6 @@ describe('SchemaCompareResult.start', function (): void { let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); const dacpacPath = mockFilePath; await result.start(dacpacPath); - let promise = new Promise(resolve => setTimeout(resolve, 5000)); // to ensure comparison result view is initialized - await promise; should.notEqual(result.sourceEndpointInfo, undefined); should.equal(result.sourceEndpointInfo.endpointType, mssql.SchemaCompareEndpointType.Dacpac); @@ -111,3 +100,86 @@ describe('SchemaCompareResult.start', function (): void { should.equal(result.targetEndpointInfo, undefined); }); }); + +describe('SchemaCompareMainWindow.execute', function (): void { + before(() => { + mockExtensionContext = TypeMoq.Mock.ofType(); + mockExtensionContext.setup(x => x.extensionPath).returns(() => ''); + testContext = createContext(); + }); + + beforeEach(async function (): Promise { + testContext.apiWrapper.reset(); + }); + + it('Should fail for failing Schema Compare service', async function (): Promise { + let sc = new SchemaCompareTestService(testStateScmp.FAILURE); + + testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); }); + let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); + + await result.start(null); + + should(result.getComparisonResult() === undefined); + + result.sourceEndpointInfo = await setDacpacEndpointInfo(mocksource); + result.targetEndpointInfo = await setDacpacEndpointInfo(mocktarget); + + await shouldThrowSpecificError(async () => await result.execute(), loc.compareErrorMessage('Test failure')); + }); + + it('Should exit for failing Schema Compare service', async function (): Promise { + let sc = new SchemaCompareTestService(testStateScmp.FAILURE); + + testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); + + await result.start(null); + + should(result.getComparisonResult() === undefined); + + result.sourceEndpointInfo = await setDacpacEndpointInfo(mocksource); + result.targetEndpointInfo = await setDacpacEndpointInfo(mocktarget); + + await result.execute(); + testContext.apiWrapper.verify(x => x.showErrorMessage(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + it('Should disable script button and apply button for Schema Compare service for dacpac', async function (): Promise { + let sc = new SchemaCompareTestService(testStateScmp.SUCCESS_NOT_EQUAL); + + let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); + + await result.start(null); + + should(result.getComparisonResult() === undefined); + + result.sourceEndpointInfo = await setDacpacEndpointInfo(mocksource); + result.targetEndpointInfo = await setDacpacEndpointInfo(mocktarget); + + await result.execute(); + + //Generate script button and apply button should be disabled for dacpac comparison + should(result.verifyButtonsState(false, false)).equal(true); + }); + + it('Should disable script button and apply button for Schema Compare service for database', async function (): Promise { + let sc = new SchemaCompareTestService(testStateScmp.SUCCESS_NOT_EQUAL); + + let result = new SchemaCompareMainWindow(testContext.apiWrapper.object, sc, mockExtensionContext.object); + + await result.start(null); + + should(result.getComparisonResult() === undefined); + + result.sourceEndpointInfo = await setDacpacEndpointInfo(mocksource); + result.targetEndpointInfo = await setDatabaseEndpointInfo(); + + await result.execute(); + + //Generate script button and apply button should be enabled for database comparison + should(result.verifyButtonsState(true, true)).equal(true); + }); + +}); + diff --git a/extensions/schema-compare/src/test/testSchemaCompareService.ts b/extensions/schema-compare/src/test/testSchemaCompareService.ts index f4e43a5dfe..12366e90b3 100644 --- a/extensions/schema-compare/src/test/testSchemaCompareService.ts +++ b/extensions/schema-compare/src/test/testSchemaCompareService.ts @@ -9,6 +9,16 @@ import * as mssql from '../../../mssql'; export class SchemaCompareTestService implements mssql.ISchemaCompareService { testOperationId: string = 'Test Operation Id'; + testState: testStateScmp; + + constructor(state?: testStateScmp) { + if (state) { + this.testState = state; + } + else { + this.testState = testStateScmp.SUCCESS_EQUAL; + } + } schemaComparePublishChanges(operationId: string, targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { throw new Error('Method not implemented.'); @@ -38,13 +48,56 @@ export class SchemaCompareTestService implements mssql.ISchemaCompareService { } schemaCompare(operationId: string, sourceEndpointInfo: mssql.SchemaCompareEndpointInfo, targetEndpointInfo: mssql.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable { - let result: mssql.SchemaCompareResult = { - operationId: this.testOperationId, - areEqual: true, - differences: [], - success: true, - errorMessage: '' - }; + let result: mssql.SchemaCompareResult; + if (this.testState === testStateScmp.FAILURE) { + result = { + operationId: this.testOperationId, + areEqual: false, + differences: [], + success: false, + errorMessage: 'Test failure' + }; + } + else if (this.testState === testStateScmp.SUCCESS_NOT_EQUAL) { + result = { + operationId: this.testOperationId, + areEqual: false, + differences: [{ + updateAction: 2, + differenceType: 0, + name: 'SqlTable', + sourceValue: ['dbo', 'table1'], + targetValue: null, + parent: null, + children: [{ + updateAction: 2, + differenceType: 0, + name: 'SqlSimpleColumn', + sourceValue: ['dbo', 'table1', 'id'], + targetValue: null, + parent: null, + children: [], + sourceScript: '', + targetScript: null, + included: false + }], + sourceScript: 'CREATE TABLE [dbo].[table1](id int)', + targetScript: null, + included: true + }], + success: true, + errorMessage: '' + }; + } + else { + result = { + operationId: this.testOperationId, + areEqual: true, + differences: [], + success: true, + errorMessage: null + }; + } return Promise.resolve(result); } @@ -63,3 +116,9 @@ export class SchemaCompareTestService implements mssql.ISchemaCompareService { registerOnUpdated(handler: () => any): void { } } + +export enum testStateScmp { + SUCCESS_EQUAL, + SUCCESS_NOT_EQUAL, + FAILURE +} diff --git a/extensions/schema-compare/src/test/testUtils.ts b/extensions/schema-compare/src/test/testUtils.ts index c6436907f4..08e40c65d5 100644 --- a/extensions/schema-compare/src/test/testUtils.ts +++ b/extensions/schema-compare/src/test/testUtils.ts @@ -99,3 +99,24 @@ export async function shouldThrowSpecificError(block: Function, expectedMessage: throw new AssertionError({ message: `Operation succeeded, but expected failure with exception: "${expectedMessage}".${details ? ' ' + details : ''}` }); } } + +export async function setDacpacEndpointInfo(path: string): Promise { + let endpointInfo: mssql.SchemaCompareEndpointInfo; + + endpointInfo = { ...mockDacpacEndpoint }; + endpointInfo.packageFilePath = path; + + return endpointInfo; +} + +export async function setDatabaseEndpointInfo(): Promise { + let endpointInfo: mssql.SchemaCompareEndpointInfo; + let dbName = 'My Database'; + let serverName = 'My Server'; + + endpointInfo = { ...mockDatabaseEndpoint }; + endpointInfo.databaseName = dbName; + endpointInfo.serverName = serverName; + + return endpointInfo; +}