diff --git a/extensions/resource-deployment/src/test/stubs.ts b/extensions/resource-deployment/src/test/stubs.ts index 922561585c..e448c7cf48 100644 --- a/extensions/resource-deployment/src/test/stubs.ts +++ b/extensions/resource-deployment/src/test/stubs.ts @@ -3,8 +3,11 @@ * 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 { @@ -103,3 +106,117 @@ 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 new file mode 100644 index 0000000000..f26b162c3c --- /dev/null +++ b/extensions/resource-deployment/src/test/ui/validation/modelViewUtils.test.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'mocha'; +import * as vscode from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { initializeWizardPage, InputComponent, InputComponentInfo, Validator, WizardPageContext } from '../../../ui/modelViewUtils'; +import { FieldType } from '../../../interfaces'; +import { IToolsService } from '../../../services/toolsService'; +import { Deferred } from '../../utils'; +import { createMockComponentBuilder, createModelViewMock as createMockModelView, StubCheckbox, StubInputBox } from '../../stubs'; +import * as should from 'should'; +import * as sinon from 'sinon'; + + +describe('WizardPage', () => { + let mockModelBuilder: TypeMoq.IMock; + let testWizardPage: WizardPageContext; + let contentRegistered: Deferred; + + before(function () { + contentRegistered = new Deferred(); + const mockWizardPage = TypeMoq.Mock.ofType(); + const mockModelView = createMockModelView(); + mockModelBuilder = mockModelView.modelBuilder; + mockWizardPage.setup(p => p.registerContent(TypeMoq.It.isAny())).callback(async (handler: (view: azdata.ModelView) => Thenable) => { + await handler(mockModelView.modelView.object); + contentRegistered.resolve(); + }); + const mockWizard = TypeMoq.Mock.ofType(); + const mockToolsService = TypeMoq.Mock.ofType(); + testWizardPage = { + page: mockWizardPage.object, + container: mockWizard.object, + wizardInfo: { + title: 'TestWizard', + pages: [], + doneAction: {} + }, + pageInfo: { + title: 'TestWizardPage', + sections: [ + { + fields: [ + { + label: 'Field1', + type: FieldType.Checkbox + }, + { + label: 'Field2', + type: FieldType.Text, + enabled: { + target: 'Field1', + value: 'true' + } + } + ] + } + ] + }, + inputComponents: {}, + onNewDisposableCreated: (_disposable: vscode.Disposable): void => { }, + onNewInputComponentCreated: ( + name: string, + inputComponentInfo: InputComponentInfo + ): void => { + testWizardPage.inputComponents[name] = inputComponentInfo; + }, + onNewValidatorCreated: (_validator: Validator): void => { }, + toolsService: mockToolsService.object + }; + }); + + 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; + sinon.stub(stubInputBox, 'enabled').set(v => { + enabled = v; + enabledDeferred.resolve(); + }); + 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; + await enabledDeferred.promise; + console.log(stubInputBox.enabled); + should(stubInputBox.enabled).be.false('Input box should be disabled by default'); + enabledDeferred = new Deferred(); + stubCheckbox.checked = true; + // Now wait for the enabled state to be updated again + await enabledDeferred.promise; + should(stubInputBox.enabled).be.true('Input box should be enabled after target component value updated'); + }); +});