From 7f38e87ef3a94be949326ce3bdd1192f44202aa9 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Wed, 16 Dec 2020 16:28:02 -0800 Subject: [PATCH] Use azdata-test modelview stubs (#13818) --- extensions/arc/package.json | 1 + .../arc/src/test/mocks/fakeRadioButton.ts | 91 -------------- extensions/arc/src/test/stubs.ts | 107 +--------------- .../src/test/ui/components/filePicker.test.ts | 23 ++-- .../ui/components/radioOptionsGroup.test.ts | 53 +++----- .../arc/src/ui/components/filePicker.ts | 22 ++-- .../src/ui/components/radioOptionsGroup.ts | 4 + .../src/ui/dialogs/connectControllerDialog.ts | 2 +- extensions/arc/yarn.lock | 47 ++++++- extensions/resource-deployment/package.json | 1 + .../resource-deployment/src/test/stubs.ts | 117 ------------------ .../test/ui/validation/modelViewUtils.test.ts | 25 ++-- extensions/resource-deployment/yarn.lock | 45 +++++++ 13 files changed, 152 insertions(+), 386 deletions(-) delete mode 100644 extensions/arc/src/test/mocks/fakeRadioButton.ts diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 1e24296bbf..54c65e9781 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -1072,6 +1072,7 @@ "@types/sinon": "^9.0.4", "@types/uuid": "^8.3.0", "@types/yamljs": "^0.2.31", + "azdata-test": "^1.0.0", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", diff --git a/extensions/arc/src/test/mocks/fakeRadioButton.ts b/extensions/arc/src/test/mocks/fakeRadioButton.ts deleted file mode 100644 index a1030f9aab..0000000000 --- a/extensions/arc/src/test/mocks/fakeRadioButton.ts +++ /dev/null @@ -1,91 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as azdata from 'azdata'; -import * as vscode from 'vscode'; - -export class FakeRadioButton implements azdata.RadioButtonComponent { - - private _onDidClickEmitter = new vscode.EventEmitter(); - - onDidClick = this._onDidClickEmitter.event; - - constructor(props: azdata.RadioButtonProperties) { - this.label = props.label; - this.value = props.value; - this.checked = props.checked; - this.enabled = props.enabled; - } - - //#region RadioButtonProperties implementation - label?: string; - value?: string; - checked?: boolean; - //#endregion - - click() { - this.checked = true; - this._onDidClickEmitter.fire(this); - } - //#region Component Implementation - id: string = ''; - updateProperties(_properties: { [key: string]: any; }): Thenable { - throw new Error('Method not implemented.'); - } - updateProperty(_key: string, _value: any): Thenable { - throw new Error('Method not implemented.'); - } - updateCssStyles(_cssStyles: { [key: string]: string; }): Thenable { - throw new Error('Method not implemented.'); - } - onValidityChanged: vscode.Event = >{}; - valid: boolean = false; - validate(): Thenable { - throw new Error('Method not implemented.'); - } - focus(): Thenable { - throw new Error('Method not implemented.'); - } - ariaHidden?: boolean | undefined; - //#endregion - - //#region ComponentProperties Implementation - height?: number | string; - width?: number | string; - /** - * The position CSS property. Empty by default. - * This is particularly useful if laying out components inside a FlexContainer and - * the size of the component is meant to be a fixed size. In this case the position must be - * set to 'absolute', with the parent FlexContainer having 'relative' position. - * Without this the component will fail to correctly size itself - */ - position?: azdata.PositionType; - /** - * Whether the component is enabled in the DOM - */ - enabled?: boolean; - /** - * Corresponds to the display CSS property for the element - */ - display?: azdata.DisplayType; - /** - * Corresponds to the aria-label accessibility attribute for this component - */ - ariaLabel?: string; - /** - * Corresponds to the role accessibility attribute for this component - */ - ariaRole?: string; - /** - * Corresponds to the aria-selected accessibility attribute for this component - */ - ariaSelected?: boolean; - /** - * Matches the CSS style key and its available values. - */ - CSSStyles?: { [key: string]: string }; - //#endregion - -} diff --git a/extensions/arc/src/test/stubs.ts b/extensions/arc/src/test/stubs.ts index 741478bc21..c7dd1aae44 100644 --- a/extensions/arc/src/test/stubs.ts +++ b/extensions/arc/src/test/stubs.ts @@ -3,107 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as azdata from 'azdata'; -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; -interface ModelViewMocks { - mockModelView: TypeMoq.IMock, - mockModelBuilder: TypeMoq.IMock, - mockTextBuilder: TypeMoq.IMock>, - mockInputBoxBuilder: TypeMoq.IMock>, - mockButtonBuilder: TypeMoq.IMock>, - mockRadioButtonBuilder: TypeMoq.IMock>, - mockDivBuilder: TypeMoq.IMock, - mockFlexBuilder: TypeMoq.IMock, - mockLoadingBuilder: TypeMoq.IMock -} - -export function createModelViewMock(buttonClickEmitter?: vscode.EventEmitter): ModelViewMocks { - const mockModelBuilder = TypeMoq.Mock.ofType(); - const mockTextBuilder = setupMockComponentBuilder(); - const mockInputBoxBuilder = setupMockComponentBuilder(); - buttonClickEmitter = buttonClickEmitter ?? new vscode.EventEmitter(); - const mockButtonBuilder = setupMockButtonBuilderWithClickEmitter(buttonClickEmitter); - const mockRadioButtonBuilder = setupMockComponentBuilder(); - const mockDivBuilder = setupMockContainerBuilder(); - const mockFlexBuilder = setupMockContainerBuilder(); - const mockLoadingBuilder = setupMockLoadingBuilder(); - mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object); - mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object); - mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object); - mockModelBuilder.setup(b => b.button()).returns(() => mockButtonBuilder.object); - mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object); - mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object); - mockModelBuilder.setup(b => b.flexContainer()).returns(() => mockFlexBuilder.object); - const mockModelView = TypeMoq.Mock.ofType(); - mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object); - return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockButtonBuilder, mockRadioButtonBuilder, mockDivBuilder, mockFlexBuilder, mockLoadingBuilder }; -} - -function setupMockButtonBuilderWithClickEmitter(buttonClickEmitter: vscode.EventEmitter): TypeMoq.IMock> { - const { mockComponentBuilder: mockButtonBuilder, mockComponent: mockButtonComponent } = setupMockComponentBuilderAndComponent(); - mockButtonComponent.setup(b => b.onDidClick(TypeMoq.It.isAny())).returns(buttonClickEmitter.event); - return mockButtonBuilder; -} - -function setupMockLoadingBuilder( - loadingBuilderGetter?: (item: azdata.Component) => azdata.LoadingComponentBuilder, - mockLoadingBuilder?: TypeMoq.IMock -): TypeMoq.IMock { - mockLoadingBuilder = mockLoadingBuilder ?? setupMockComponentBuilder(); - let item: azdata.Component; - mockLoadingBuilder.setup(b => b.withItem(TypeMoq.It.isAny())).callback((_item) => item = _item).returns(() => loadingBuilderGetter ? loadingBuilderGetter(item) : mockLoadingBuilder!.object); - return mockLoadingBuilder; -} - -export function setupMockComponentBuilder = azdata.ComponentBuilder>( - componentGetter?: (props: P) => T, - mockComponentBuilder?: TypeMoq.IMock, -): TypeMoq.IMock { - mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType(); - setupMockComponentBuilderAndComponent(mockComponentBuilder, componentGetter); - return mockComponentBuilder; -} - -function setupMockComponentBuilderAndComponent = azdata.ComponentBuilder>( - mockComponentBuilder?: TypeMoq.IMock, - componentGetter?: ((props: P) => T) -): { mockComponentBuilder: TypeMoq.IMock, mockComponent: TypeMoq.IMock } { - mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType(); - const mockComponent = createComponentMock(); - let compProps: P; - mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object); - mockComponentBuilder.setup(b => b.component()).returns(() => { - return componentGetter ? componentGetter(compProps) : Object.assign(Object.assign({}, mockComponent.object), compProps); - }); - - // For now just have these be passthrough - can hook up additional functionality later if needed - mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object); - return { mockComponentBuilder, mockComponent }; -} - -function createComponentMock(): TypeMoq.IMock { - const mockComponent = TypeMoq.Mock.ofType(); - // Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66 - mockComponent.setup((x: any) => x.then).returns(() => { }); - return mockComponent; -} - -export function setupMockContainerBuilder, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder = azdata.ContainerBuilder>( - mockContainerBuilder?: TypeMoq.IMock -): TypeMoq.IMock { - const items: azdata.Component[] = []; - const mockContainer = createComponentMock(); // T is azdata.Container type so this creates a azdata.Container mock - mockContainer.setup(c => c.items).returns(() => items); - mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder((_props) => mockContainer.object); - - mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback((_items, _itemsStyle) => items.push(..._items)).returns(() => mockContainerBuilder!.object); - // For now just have these be passthrough - can hook up additional functionality later if needed - mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object); - return mockContainerBuilder; -} - export class MockInputBox implements vscode.InputBox { private _value: string = ''; public get value(): string { @@ -118,17 +19,17 @@ export class MockInputBox implements vscode.InputBox { placeholder: string | undefined; password: boolean = false; private _onDidChangeValueCallback: ((e: string) => any) | undefined = undefined; - onDidChangeValue: vscode.Event = (listener) => { + onDidChangeValue: vscode.Event = (listener: (value: string) => void) => { this._onDidChangeValueCallback = listener; return new vscode.Disposable(() => { }); }; private _onDidAcceptCallback: ((e: void) => any) | undefined = undefined; - public onDidAccept: vscode.Event = (listener) => { + public onDidAccept: vscode.Event = (listener: () => void) => { this._onDidAcceptCallback = listener; return new vscode.Disposable(() => { }); }; buttons: readonly vscode.QuickInputButton[] = []; - onDidTriggerButton: vscode.Event = (_) => { return new vscode.Disposable(() => { }); }; + onDidTriggerButton: vscode.Event = () => { return new vscode.Disposable(() => { }); }; prompt: string | undefined; validationMessage: string | undefined; title: string | undefined; @@ -145,7 +46,7 @@ export class MockInputBox implements vscode.InputBox { } } private _onDidHideCallback: ((e: void) => any) | undefined = undefined; - onDidHide: vscode.Event = (listener) => { + onDidHide: vscode.Event = (listener: () => void) => { this._onDidHideCallback = listener; return new vscode.Disposable(() => { }); }; diff --git a/extensions/arc/src/test/ui/components/filePicker.test.ts b/extensions/arc/src/test/ui/components/filePicker.test.ts index 65d9302662..5b70172eb3 100644 --- a/extensions/arc/src/test/ui/components/filePicker.test.ts +++ b/extensions/arc/src/test/ui/components/filePicker.test.ts @@ -3,29 +3,22 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as azdata from 'azdata'; import * as path from 'path'; import * as should from 'should'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; import { Deferred } from '../../../common/promise'; import { FilePicker } from '../../../ui/components/filePicker'; -import { createModelViewMock } from '../../stubs'; +import { createModelViewMock } from 'azdata-test/out/mocks/modelView/modelViewMock'; +import { StubButton } from 'azdata-test/out/stubs/modelView/stubButton'; let filePicker: FilePicker; const initialPath = path.join('path', 'to', '.kube','config'); const newFileUri = vscode.Uri.file(path.join('path', 'to', 'new', '.kube', 'config')); -let filePathInputBox: azdata.InputBoxComponent; -let browseButton: azdata.ButtonComponent; -let flexContainer: azdata.FlexContainer; -const browseButtonEmitter = new vscode.EventEmitter(); describe('filePicker', function (): void { beforeEach(async () => { - const { mockModelBuilder, mockInputBoxBuilder, mockButtonBuilder, mockFlexBuilder } = createModelViewMock(browseButtonEmitter); - filePicker = new FilePicker(mockModelBuilder.object, initialPath, (_disposable) => { }); - filePathInputBox = mockInputBoxBuilder.object.component(); - browseButton = mockButtonBuilder.object.component(); - flexContainer = mockFlexBuilder.object.component(); + const { modelBuilderMock } = createModelViewMock(); + filePicker = new FilePicker(modelBuilderMock.object, initialPath, (_disposable) => { }); }); afterEach(() => { @@ -33,22 +26,22 @@ describe('filePicker', function (): void { }); it('browse Button chooses new FilePath', async () => { - should(filePathInputBox.value).should.not.be.undefined(); + should(filePicker.filePathInputBox.value).should.not.be.undefined(); filePicker.value!.should.equal(initialPath); - flexContainer.items.should.deepEqual([filePathInputBox, browseButton]); + filePicker.component().items.length.should.equal(2, 'Filepicker container should have two components'); const deferred = new Deferred(); sinon.stub(vscode.window, 'showOpenDialog').callsFake(async (_options) => { deferred.resolve(); return [newFileUri]; }); - browseButtonEmitter.fire(undefined); //simulate the click of the browseButton + (filePicker.filePickerButton as StubButton).click(); await deferred; filePicker.value!.should.equal(newFileUri.fsPath); }); describe('getters and setters', async () => { it('component getter', () => { - should(filePicker.component()).equal(flexContainer); + should(filePicker.component()).not.be.undefined(); }); [true, false].forEach(testValue => { it(`Test readOnly with testValue: ${testValue}`, () => { diff --git a/extensions/arc/src/test/ui/components/radioOptionsGroup.test.ts b/extensions/arc/src/test/ui/components/radioOptionsGroup.test.ts index 9d9c931e83..22f3762857 100644 --- a/extensions/arc/src/test/ui/components/radioOptionsGroup.test.ts +++ b/extensions/arc/src/test/ui/components/radioOptionsGroup.test.ts @@ -7,43 +7,26 @@ import * as azdata from 'azdata'; import * as should from 'should'; import { getErrorMessage } from '../../../common/utils'; import { RadioOptionsGroup, RadioOptionsInfo } from '../../../ui/components/radioOptionsGroup'; -import { FakeRadioButton } from '../../mocks/fakeRadioButton'; -import { setupMockComponentBuilder, createModelViewMock } from '../../stubs'; +import { createModelViewMock } from 'azdata-test/out/mocks/modelView/modelViewMock'; +import { StubRadioButton } from 'azdata-test/out/stubs/modelView/stubRadioButton'; const loadingError = new Error('Error loading options'); -const radioOptionsInfo = { +const radioOptionsInfo: RadioOptionsInfo = { values: [ 'value1', 'value2' ], defaultValue: 'value2' }; -const divItems: azdata.Component[] = []; + let radioOptionsGroup: RadioOptionsGroup; -let loadingComponent: azdata.LoadingComponent; describe('radioOptionsGroup', function (): void { beforeEach(async () => { - const { mockModelBuilder, mockRadioButtonBuilder, mockDivBuilder, mockLoadingBuilder } = createModelViewMock(); - mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own. - setupMockComponentBuilder( - (props) => new FakeRadioButton(props), - mockRadioButtonBuilder, - ); - mockDivBuilder.reset(); // reset previous setups so new setups we are about to create will replace the setups instead creating a recording chain - // create new setups for the DivContainer with custom behavior - setupMockComponentBuilder( - () => { - addItem: (item) => { divItems.push(item); }, - clearItems: () => { divItems.length = 0; }, - get items() { return divItems; }, - }, - mockDivBuilder - ); - radioOptionsGroup = new RadioOptionsGroup(mockModelBuilder.object, (_disposable) => { }); + const { modelBuilderMock } = createModelViewMock(); + radioOptionsGroup = new RadioOptionsGroup(modelBuilderMock.object, (_disposable) => { }); await radioOptionsGroup.load(async () => radioOptionsInfo); - loadingComponent = mockLoadingBuilder.object.component(); }); it('verify construction and load', async () => { @@ -56,17 +39,17 @@ describe('radioOptionsGroup', function (): void { it('onClick', async () => { // click the radioButton corresponding to 'value1' - (divItems as FakeRadioButton[]).filter(r => r.value === 'value1').pop()!.click(); + ((radioOptionsGroup.items as azdata.RadioButtonComponent[]).find(r => r.value === 'value1') as StubRadioButton).click(); radioOptionsGroup.value!.should.equal('value1', 'radio options group should correspond to the radioButton that we clicked'); // verify all the radioButtons created in the group verifyRadioGroup(); }); it('load throws', async () => { - radioOptionsGroup.load(() => { throw loadingError; }); + await radioOptionsGroup.load(() => { throw loadingError; }); //in error case radioButtons array wont hold radioButtons but holds a TextComponent with value equal to error string - divItems.length.should.equal(1, 'There is should be only one element in the divContainer when loading error happens'); - const label = divItems[0] as azdata.TextComponent; + radioOptionsGroup.items.length.should.equal(1, 'There is should be only one element in the divContainer when loading error happens'); + const label = radioOptionsGroup.items[0] as azdata.TextComponent; should(label.value).not.be.undefined(); label.value!.should.deepEqual(getErrorMessage(loadingError)); should(label.CSSStyles).not.be.undefined(); @@ -76,7 +59,7 @@ describe('radioOptionsGroup', function (): void { describe('getters and setters', async () => { it(`component getter`, () => { - radioOptionsGroup.component().should.deepEqual(loadingComponent); + should(radioOptionsGroup.component()).not.be.undefined(); }); [true, false].forEach(testValue => { @@ -93,14 +76,14 @@ describe('radioOptionsGroup', function (): void { }); function verifyRadioGroup() { - const radioButtons = divItems as FakeRadioButton[]; - radioButtons.length.should.equal(radioOptionsInfo.values!.length); + const radioButtons = radioOptionsGroup.items as azdata.RadioButtonComponent[]; + radioButtons.length.should.equal(radioOptionsInfo.values!.length, 'Unexpected number of radio buttons'); radioButtons.forEach(rb => { - should(rb.label).not.be.undefined(); - should(rb.value).not.be.undefined(); - should(rb.enabled).not.be.undefined(); - rb.label!.should.equal(rb.value); - rb.enabled!.should.be.true(); + should(rb.label).not.equal(undefined, 'Radio Button label should not be undefined'); + should(rb.value).not.equal(undefined, 'Radio button value should not be undefined'); + should(rb.enabled).not.equal(undefined, 'Enabled should not be undefined'); + rb.label!.should.equal(rb.value, 'Radio button label did not match'); + rb.enabled!.should.be.true('Radio button should be enabled'); }); } diff --git a/extensions/arc/src/ui/components/filePicker.ts b/extensions/arc/src/ui/components/filePicker.ts index cebba3bdd0..793d8b4230 100644 --- a/extensions/arc/src/ui/components/filePicker.ts +++ b/extensions/arc/src/ui/components/filePicker.ts @@ -15,30 +15,30 @@ export interface RadioOptionsInfo { export class FilePicker implements IReadOnly { private _flexContainer: azdata.FlexContainer; - private _filePathInputBox: azdata.InputBoxComponent; - private _filePickerButton: azdata.ButtonComponent; + public readonly filePathInputBox: azdata.InputBoxComponent; + public readonly filePickerButton: azdata.ButtonComponent; constructor( modelBuilder: azdata.ModelBuilder, initialPath: string, onNewDisposableCreated: (disposable: vscode.Disposable) => void ) { const buttonWidth = 80; - this._filePathInputBox = modelBuilder.inputBox() + this.filePathInputBox = modelBuilder.inputBox() .withProperties({ value: initialPath, width: 350 }).component(); - this._filePickerButton = modelBuilder.button() + this.filePickerButton = modelBuilder.button() .withProperties({ label: loc.browse, width: buttonWidth }).component(); - onNewDisposableCreated(this._filePickerButton.onDidClick(async () => { + onNewDisposableCreated(this.filePickerButton.onDidClick(async () => { const fileUris = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - defaultUri: this._filePathInputBox.value ? vscode.Uri.file(path.dirname(this._filePathInputBox.value)) : undefined, + defaultUri: this.filePathInputBox.value ? vscode.Uri.file(path.dirname(this.filePathInputBox.value)) : undefined, openLabel: loc.select, filters: undefined /* file type filters */ }); @@ -47,21 +47,21 @@ export class FilePicker implements IReadOnly { return; // This can happen when a user cancels out. we don't throw and the user just won't be able to move on until they select something. } const fileUri = fileUris[0]; //we allow the user to select only one file in the dialog - this._filePathInputBox.value = fileUri.fsPath; + this.filePathInputBox.value = fileUri.fsPath; })); - this._flexContainer = createFlexContainer(modelBuilder, [this._filePathInputBox, this._filePickerButton]); + this._flexContainer = createFlexContainer(modelBuilder, [this.filePathInputBox, this.filePickerButton]); } - component(): azdata.Component { + component(): azdata.FlexContainer { return this._flexContainer; } get onTextChanged() { - return this._filePathInputBox.onTextChanged; + return this.filePathInputBox.onTextChanged; } get value(): string | undefined { - return this._filePathInputBox?.value; + return this.filePathInputBox?.value; } get readOnly(): boolean { diff --git a/extensions/arc/src/ui/components/radioOptionsGroup.ts b/extensions/arc/src/ui/components/radioOptionsGroup.ts index 7846efa9c1..5a19ca4213 100644 --- a/extensions/arc/src/ui/components/radioOptionsGroup.ts +++ b/extensions/arc/src/ui/components/radioOptionsGroup.ts @@ -84,4 +84,8 @@ export class RadioOptionsGroup implements IReadOnly { this._divContainer.items.forEach(r => r.enabled = value); this._divContainer.enabled = value; } + + get items(): azdata.Component[] { + return this._divContainer.items; + } } diff --git a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts index 6cd0a27647..2abfaded1a 100644 --- a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts +++ b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts @@ -157,7 +157,7 @@ abstract class ControllerDialogBase extends InitializingComponent { return this.dialog; } - public abstract async validate(): Promise; + public abstract validate(): Promise; private handleCancel(): void { this.completionPromise.resolve(undefined); diff --git a/extensions/arc/yarn.lock b/extensions/arc/yarn.lock index 36f59e1832..f087d7ab37 100644 --- a/extensions/arc/yarn.lock +++ b/extensions/arc/yarn.lock @@ -275,6 +275,13 @@ resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245" integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ== +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + ajv@^6.5.5: version "6.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" @@ -338,6 +345,16 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== +azdata-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/azdata-test/-/azdata-test-1.0.0.tgz#b8821a5a313e6919dac8b8ef56f22dc8592cad2c" + integrity sha512-/QZkHeVKc0HbJtx45nFXNQLTe3lY8la37Re+YfBIzybMIul3FmBszwIyq9/MPHMhrjkvf5xDkEF+CuSlI4KeIA== + dependencies: + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.4" + rimraf "^2.6.3" + typemoq "^2.1.0" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -510,6 +527,18 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -647,6 +676,14 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -656,6 +693,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1204,7 +1249,7 @@ type-detect@4.0.8, type-detect@^4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -typemoq@2.1.0: +typemoq@2.1.0, typemoq@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw== diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json index 0ca36f5d7e..ce4515e82c 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -524,6 +524,7 @@ "@types/semver": "^7.3.1", "@types/sinon": "^9.0.8", "@types/yamljs": "0.2.30", + "azdata-test": "^1.0.0", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", diff --git a/extensions/resource-deployment/src/test/stubs.ts b/extensions/resource-deployment/src/test/stubs.ts index e448c7cf48..922561585c 100644 --- a/extensions/resource-deployment/src/test/stubs.ts +++ b/extensions/resource-deployment/src/test/stubs.ts @@ -3,11 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as azdata from 'azdata'; -import * as vscode from 'vscode'; import * as events from 'events'; import * as cp from 'promisify-child-process'; -import * as TypeMoq from 'typemoq'; import { Readable } from 'stream'; export class TestChildProcessPromise implements cp.ChildProcessPromise { @@ -106,117 +103,3 @@ export class TestChildProcessPromise implements cp.ChildProcessPromise { throw new Error('Method not implemented.'); } } - -export type ComponentAndMockComponentBuilder = { - component: C, - mockBuilder: TypeMoq.IMock -}; - -export function createModelViewMock(): { - modelBuilder: TypeMoq.IMock, - modelView: TypeMoq.IMock -} { - const mockModelView = TypeMoq.Mock.ofType(); - const mockModelBuilder = TypeMoq.Mock.ofType(); - const mockTextBuilder = createMockComponentBuilder(); - const mockGroupContainerBuilder = createMockContainerBuilder(); - const mockFormContainerBuilder = createMockFormContainerBuilder(); - mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.mockBuilder.object); - mockModelBuilder.setup(b => b.groupContainer()).returns(() => mockGroupContainerBuilder.mockBuilder.object); - mockModelBuilder.setup(b => b.formContainer()).returns(() => mockFormContainerBuilder.object); - mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object); - return { - modelBuilder: mockModelBuilder, - modelView: mockModelView - }; -} - -export function createMockComponentBuilder = azdata.ComponentBuilder>(component?: C): ComponentAndMockComponentBuilder { - const mockComponentBuilder = TypeMoq.Mock.ofType(); - // Create a mocked dynamic component if we don't have a stub instance to use. - // Note that we don't use ofInstance here for the component because there's some limitations around properties that I was - // hitting preventing me from easily using TypeMoq. Passing in the stub instance lets users control the object being stubbed - which means - // they can use things like sinon to then override specific functions if desired. - if (!component) { - const mockComponent = TypeMoq.Mock.ofType(); - // Need to setup then for when a dynamic mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66 - mockComponent.setup((x: any) => x.then).returns(() => undefined); - component = mockComponent.object; - } - // For now just have these be passthrough - can hook up additional functionality later if needed - mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).returns(() => mockComponentBuilder.object); - mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder.object); - mockComponentBuilder.setup(b => b.component()).returns(() => component! /*mockComponent.object*/); - return { - component: component!, - mockBuilder: mockComponentBuilder - }; -} - -export function createMockContainerBuilder, B extends azdata.ContainerBuilder = azdata.ContainerBuilder>(): ComponentAndMockComponentBuilder { - const mockContainerBuilder = createMockComponentBuilder(); - // For now just have these be passthrough - can hook up additional functionality later if needed - mockContainerBuilder.mockBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder.mockBuilder.object); - mockContainerBuilder.mockBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder.mockBuilder.object); - return mockContainerBuilder; -} - -export function createMockFormContainerBuilder(): TypeMoq.IMock { - const mockContainerBuilder = createMockContainerBuilder(); - mockContainerBuilder.mockBuilder.setup(b => b.withFormItems(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => mockContainerBuilder.mockBuilder.object); - return mockContainerBuilder.mockBuilder; -} - -export class StubInputBox implements azdata.InputBoxComponent { - readonly id = 'input-box'; - public enabled: boolean = false; - - onTextChanged: vscode.Event = undefined!; - onEnterKeyPressed: vscode.Event = undefined!; - - updateProperties(properties: { [key: string]: any }): Thenable { throw new Error('Not implemented'); } - - updateProperty(key: string, value: any): Thenable { throw new Error('Not implemented'); } - - updateCssStyles(cssStyles: { [key: string]: string }): Thenable { throw new Error('Not implemented'); } - - readonly onValidityChanged: vscode.Event = undefined!; - - readonly valid: boolean = true; - - validate(): Thenable { throw new Error('Not implemented'); } - - focus(): Thenable { return Promise.resolve(); } -} - -export class StubCheckbox implements azdata.CheckBoxComponent { - private _onChanged = new vscode.EventEmitter(); - private _checked = false; - - readonly id = 'stub-checkbox'; - public enabled: boolean = false; - - get checked(): boolean { - return this._checked; - } - set checked(value: boolean) { - this._checked = value; - this._onChanged.fire(); - } - - onChanged: vscode.Event = this._onChanged.event; - - updateProperties(properties: { [key: string]: any }): Thenable { throw new Error('Not implemented'); } - - updateProperty(key: string, value: any): Thenable { throw new Error('Not implemented'); } - - updateCssStyles(cssStyles: { [key: string]: string }): Thenable { throw new Error('Not implemented'); } - - readonly onValidityChanged: vscode.Event = undefined!; - - readonly valid: boolean = true; - - validate(): Thenable { throw new Error('Not implemented'); } - - focus(): Thenable { return Promise.resolve(); } -} diff --git a/extensions/resource-deployment/src/test/ui/validation/modelViewUtils.test.ts b/extensions/resource-deployment/src/test/ui/validation/modelViewUtils.test.ts index 3575fe21dd..8eabaed7c0 100644 --- a/extensions/resource-deployment/src/test/ui/validation/modelViewUtils.test.ts +++ b/extensions/resource-deployment/src/test/ui/validation/modelViewUtils.test.ts @@ -11,23 +11,29 @@ import { initializeWizardPage, InputComponent, InputComponentInfo, Validator, Wi import { FieldType } from '../../../interfaces'; import { IToolsService } from '../../../services/toolsService'; import { Deferred } from '../../utils'; -import { createMockComponentBuilder, createModelViewMock as createMockModelView, StubCheckbox, StubInputBox } from '../../stubs'; +import { createModelViewMock } from 'azdata-test/out/mocks/modelView/modelViewMock'; +import { StubCheckbox } from 'azdata-test/out/stubs/modelView/stubCheckbox'; +import { StubInputBox } from 'azdata-test/out/stubs/modelView/stubInputBox'; import * as should from 'should'; import * as sinon from 'sinon'; - describe('WizardPage', () => { - let mockModelBuilder: TypeMoq.IMock; + let stubCheckbox: StubCheckbox; + let stubInputBox: StubInputBox; let testWizardPage: WizardPageContext; let contentRegistered: Deferred; before(function () { contentRegistered = new Deferred(); const mockWizardPage = TypeMoq.Mock.ofType(); - const mockModelView = createMockModelView(); - mockModelBuilder = mockModelView.modelBuilder; + stubCheckbox = new StubCheckbox(); + stubInputBox = new StubInputBox(); + const mockModelView = createModelViewMock({ + checkBox: () => stubCheckbox, + inputBox: () => stubInputBox + }); mockWizardPage.setup(p => p.registerContent(TypeMoq.It.isAny())).callback(async (handler: (view: azdata.ModelView) => Thenable) => { - await handler(mockModelView.modelView.object); + await handler(mockModelView.modelViewMock.object); contentRegistered.resolve(); }); const mockWizard = TypeMoq.Mock.ofType(); @@ -75,9 +81,7 @@ describe('WizardPage', () => { }); it('dynamic enablement', async function (): Promise { - const stubCheckbox = new StubCheckbox(); - const mockCheckboxBuilder = createMockComponentBuilder(stubCheckbox); - const stubInputBox = new StubInputBox(); + // Stub out the enabled property so we can hook into when that's set to ensure we wait for the state to be updated // before continuing the test let enabled = false; @@ -88,11 +92,8 @@ describe('WizardPage', () => { sinon.stub(stubInputBox, 'enabled').get(() => { return enabled; }); - const mockInputBoxBuilder = createMockComponentBuilder(stubInputBox); // Used to ensure that we wait until the enabled state is updated for our mocked components before continuing let enabledDeferred = new Deferred(); - mockModelBuilder.setup(b => b.checkBox()).returns(() => mockCheckboxBuilder.mockBuilder.object); - mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.mockBuilder.object); initializeWizardPage(testWizardPage); await contentRegistered.promise; diff --git a/extensions/resource-deployment/yarn.lock b/extensions/resource-deployment/yarn.lock index b89dce2527..879502a620 100644 --- a/extensions/resource-deployment/yarn.lock +++ b/extensions/resource-deployment/yarn.lock @@ -252,6 +252,13 @@ resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.30.tgz#d034e1d329e46e8d0f737c9a8db97f68f81b5382" integrity sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I= +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" @@ -278,6 +285,16 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +azdata-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/azdata-test/-/azdata-test-1.0.0.tgz#b8821a5a313e6919dac8b8ef56f22dc8592cad2c" + integrity sha512-/QZkHeVKc0HbJtx45nFXNQLTe3lY8la37Re+YfBIzybMIul3FmBszwIyq9/MPHMhrjkvf5xDkEF+CuSlI4KeIA== + dependencies: + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.4" + rimraf "^2.6.3" + typemoq "^2.1.0" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -406,6 +423,18 @@ diff@^4.0.2: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -487,6 +516,22 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + +https-proxy-agent@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"