From 376db72d4bad3bf50e61d1eb348037fd2498dd06 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 4 Jun 2021 17:11:31 -0700 Subject: [PATCH 1/3] Add survey for MPLS usage --- package.nls.json | 1 + src/client/activation/serviceRegistry.ts | 10 ++ .../diagnostics/checks/mplsSurvey.ts | 120 ++++++++++++++++++ .../application/diagnostics/constants.ts | 1 + src/client/common/utils/localize.ts | 4 + 5 files changed, 136 insertions(+) create mode 100644 src/client/application/diagnostics/checks/mplsSurvey.ts diff --git a/package.nls.json b/package.nls.json index 01c38a8b7111..502a6e757c95 100644 --- a/package.nls.json +++ b/package.nls.json @@ -97,6 +97,7 @@ "ExtensionSurveyBanner.bannerLabelYes": "Yes, take survey now", "ExtensionSurveyBanner.bannerLabelNo": "No, thanks", "ExtensionSurveyBanner.maybeLater": "Maybe later", + "ExtensionSurveyBanner.mplsMessage": "Can you please take 2 minutes to tell us about your experience using the Microsoft Python Language Server?", "ExtensionChannels.installingInsidersMessage": "Installing Insiders... ", "ExtensionChannels.installingStableMessage": "Installing Stable... ", "ExtensionChannels.installationCompleteMessage": "complete.", diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 00c50b761af2..91d72c078c52 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -61,6 +61,11 @@ import { LanguageServerType, } from './types'; import { JediLanguageServerActivator } from './jedi/activator'; +import { IDiagnosticsService } from '../application/diagnostics/types'; +import { + MPLSSurveyDiagnosticService, + MPLSSurveyDiagnosticServiceId, +} from '../application/diagnostics/checks/mplsSurvey'; export function registerTypes(serviceManager: IServiceManager, languageServerType: LanguageServerType): void { serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); @@ -117,6 +122,11 @@ export function registerTypes(serviceManager: IServiceManager, languageServerTyp DotNetLanguageServerPackageService, ); registerDotNetTypes(serviceManager); + serviceManager.addSingleton( + IDiagnosticsService, + MPLSSurveyDiagnosticService, + MPLSSurveyDiagnosticServiceId, + ); } else if (languageServerType === LanguageServerType.Node) { serviceManager.add( ILanguageServerAnalysisOptions, diff --git a/src/client/application/diagnostics/checks/mplsSurvey.ts b/src/client/application/diagnostics/checks/mplsSurvey.ts new file mode 100644 index 000000000000..f7948624536f --- /dev/null +++ b/src/client/application/diagnostics/checks/mplsSurvey.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, named } from 'inversify'; +import { DiagnosticSeverity, env, UIKind } from 'vscode'; +import * as querystring from 'querystring'; +import { IBrowserService, IDisposableRegistry, IExtensionContext, Resource } from '../../../common/types'; +import { ExtensionSurveyBanner } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; +import { IApplicationEnvironment } from '../../../common/application/types'; +import { IPlatformService } from '../../../common/platform/types'; + +export const MPLS_SURVEY_MEMENTO = 'mplsSurveyPromptMemento'; + +export class MPLSSurveyDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.MPLSSurveyDiagnostic, + message, + DiagnosticSeverity.Information, + DiagnosticScope.Global, + resource, + ); + } +} + +export const MPLSSurveyDiagnosticServiceId = 'MPLSSurveyDiagnosticServiceId'; + +export class MPLSSurveyDiagnosticService extends BaseDiagnosticsService { + private disabledInCurrentSession = false; + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment, + @inject(IPlatformService) private platformService: IPlatformService, + @inject(IBrowserService) private browserService: IBrowserService, + ) { + super([DiagnosticCodes.MPLSSurveyDiagnostic], serviceContainer, disposableRegistry, true); + } + + public async diagnose(resource: Resource): Promise { + if (!this.shouldShowPrompt) { + return []; + } + + return [new MPLSSurveyDiagnostic(ExtensionSurveyBanner.mplsMessage(), resource)]; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + await this.messageService.handle(diagnostic, { + commandPrompts: [ + { + prompt: ExtensionSurveyBanner.bannerLabelYes(), + command: { + diagnostic, + invoke: async () => this.launchSurvey(), + }, + }, + { + prompt: ExtensionSurveyBanner.maybeLater(), + command: { + diagnostic, + invoke: async () => { + this.disabledInCurrentSession = true; + }, + }, + }, + { + prompt: ExtensionSurveyBanner.bannerLabelNo(), + command: { + diagnostic, + invoke: async () => this.updateMemento(), + }, + }, + ], + onClose: () => this.updateMemento(), + }); + } + + private async updateMemento() { + await this.context.globalState.update(MPLS_SURVEY_MEMENTO, true); + } + + private get shouldShowPrompt(): boolean { + return ( + env.uiKind !== UIKind?.Web && + !this.disabledInCurrentSession && + !this.context.globalState.get(MPLS_SURVEY_MEMENTO) + ); + } + + private launchSurvey() { + const query = querystring.stringify({ + o: encodeURIComponent(this.platformService.osType), // platform + v: encodeURIComponent(this.appEnvironment.vscodeVersion), + e: encodeURIComponent(this.appEnvironment.packageJson.version), // extension version + m: encodeURIComponent(this.appEnvironment.sessionId), + }); + const url = `https://www.surveymonkey.com/r/HD7MM9X?${query}`; + this.browserService.launch(url); + } +} diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index f03564ff2c42..09e7c16935e4 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -20,4 +20,5 @@ export enum DiagnosticCodes { ConfigPythonPathDiagnostic = 'ConfigPythonPathDiagnostic', UpgradeCodeRunnerDiagnostic = 'UpgradeCodeRunnerDiagnostic', PylanceDefaultDiagnostic = 'PylanceDefaultDiagnostic', + MPLSSurveyDiagnostic = 'MPLSSurveyDiagnostic', } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index d266f9805eef..6e0319a0b411 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -416,6 +416,10 @@ export namespace ExtensionSurveyBanner { export const bannerLabelYes = localize('ExtensionSurveyBanner.bannerLabelYes', 'Yes, take survey now'); export const bannerLabelNo = localize('ExtensionSurveyBanner.bannerLabelNo', 'No, thanks'); export const maybeLater = localize('ExtensionSurveyBanner.maybeLater', 'Maybe later'); + export const mplsMessage = localize( + 'ExtensionSurveyBanner.mplsMessage', + 'Can you please take 2 minutes to tell us about your experience using the Microsoft Python Language Server?', + ); } export namespace Products { From c2de31454dbf7860f74745d48e07c65ad15ed3fb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 7 Jun 2021 12:39:09 -0700 Subject: [PATCH 2/3] Testing --- .../diagnostics/checks/mplsSurvey.ts | 20 +- .../checks/mplsSurvey.unit.test.ts | 320 ++++++++++++++++++ 2 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts diff --git a/src/client/application/diagnostics/checks/mplsSurvey.ts b/src/client/application/diagnostics/checks/mplsSurvey.ts index f7948624536f..53073219e3d8 100644 --- a/src/client/application/diagnostics/checks/mplsSurvey.ts +++ b/src/client/application/diagnostics/checks/mplsSurvey.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// eslint-disable-next-line max-classes-per-file import { inject, named } from 'inversify'; import { DiagnosticSeverity, env, UIKind } from 'vscode'; import * as querystring from 'querystring'; @@ -71,27 +72,25 @@ export class MPLSSurveyDiagnosticService extends BaseDiagnosticsService { prompt: ExtensionSurveyBanner.bannerLabelYes(), command: { diagnostic, - invoke: async () => this.launchSurvey(), + invoke: () => this.launchSurvey(), }, }, { prompt: ExtensionSurveyBanner.maybeLater(), command: { diagnostic, - invoke: async () => { - this.disabledInCurrentSession = true; - }, + invoke: async () => this.disable(), }, }, { prompt: ExtensionSurveyBanner.bannerLabelNo(), command: { diagnostic, - invoke: async () => this.updateMemento(), + invoke: () => this.updateMemento(), }, }, ], - onClose: () => this.updateMemento(), + onClose: () => this.disable(), }); } @@ -99,6 +98,10 @@ export class MPLSSurveyDiagnosticService extends BaseDiagnosticsService { await this.context.globalState.update(MPLS_SURVEY_MEMENTO, true); } + private disable() { + this.disabledInCurrentSession = true; + } + private get shouldShowPrompt(): boolean { return ( env.uiKind !== UIKind?.Web && @@ -107,14 +110,15 @@ export class MPLSSurveyDiagnosticService extends BaseDiagnosticsService { ); } - private launchSurvey() { + private async launchSurvey() { const query = querystring.stringify({ o: encodeURIComponent(this.platformService.osType), // platform v: encodeURIComponent(this.appEnvironment.vscodeVersion), e: encodeURIComponent(this.appEnvironment.packageJson.version), // extension version m: encodeURIComponent(this.appEnvironment.sessionId), }); - const url = `https://www.surveymonkey.com/r/HD7MM9X?${query}`; + const url = `https://aka.ms/mpls-experience-survey?${query}`; this.browserService.launch(url); + await this.updateMemento(); } } diff --git a/src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts b/src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts new file mode 100644 index 000000000000..f3a347f02258 --- /dev/null +++ b/src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts @@ -0,0 +1,320 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ExtensionContext } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + MPLSSurveyDiagnostic, + MPLSSurveyDiagnosticService, + MPLS_SURVEY_MEMENTO, +} from '../../../../client/application/diagnostics/checks/mplsSurvey'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnostic, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService, +} from '../../../../client/application/diagnostics/types'; +import { IApplicationEnvironment } from '../../../../client/common/application/types'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { IBrowserService, IExtensionContext } from '../../../../client/common/types'; +import { ExtensionSurveyBanner } from '../../../../client/common/utils/localize'; +import { OSType } from '../../../../client/common/utils/platform'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - MPLS survey', () => { + let serviceContainer: typemoq.IMock; + let diagnosticService: IDiagnosticsService; + let filterService: typemoq.IMock; + let messageHandler: typemoq.IMock>; + let context: typemoq.IMock; + let memento: typemoq.IMock; + let appEnvironment: typemoq.IMock; + let platformService: typemoq.IMock; + let browserService: typemoq.IMock; + + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + filterService = typemoq.Mock.ofType(); + messageHandler = typemoq.Mock.ofType>(); + context = typemoq.Mock.ofType(); + memento = typemoq.Mock.ofType(); + appEnvironment = typemoq.Mock.ofType(); + platformService = typemoq.Mock.ofType(); + browserService = typemoq.Mock.ofType(); + + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + context.setup((c) => c.globalState).returns(() => memento.object); + + diagnosticService = new (class extends MPLSSurveyDiagnosticService { + // eslint-disable-next-line class-methods-use-this + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })( + serviceContainer.object, + context.object, + messageHandler.object, + [], + appEnvironment.object, + platformService.object, + browserService.object, + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (diagnosticService as any)._clear(); + }); + + teardown(() => { + context.reset(); + memento.reset(); + }); + + test('Should display message the prompt has not been shown yet', async () => { + memento.setup((m) => m.get(MPLS_SURVEY_MEMENTO)).returns(() => undefined); + + const diagnostics = await diagnosticService.diagnose(undefined); + + expect(diagnostics).to.be.deep.equal([ + new MPLSSurveyDiagnostic(ExtensionSurveyBanner.mplsMessage(), undefined), + ]); + }); + + test('Should return empty diagnostics if the prompt has been shown before', async () => { + memento.setup((m) => m.get(MPLS_SURVEY_MEMENTO)).returns(() => true); + + const diagnostics = await diagnosticService.diagnose(undefined); + + expect(diagnostics).to.be.lengthOf(0); + }); + + test('Should display a prompt when handling the diagnostic code', async () => { + const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + browserService.verifyAll(); + + expect(messagePrompt).to.not.be.equal(undefined); + expect(messagePrompt!.onClose).to.not.be.equal(undefined); + expect(messagePrompt!.commandPrompts).to.be.lengthOf(3); + + expect(messagePrompt!.commandPrompts[0].prompt).to.be.equal(ExtensionSurveyBanner.bannerLabelYes()); + expect(messagePrompt!.commandPrompts[0].command).to.not.be.equal(undefined); + expect(messagePrompt!.commandPrompts[1].prompt).to.be.equal(ExtensionSurveyBanner.maybeLater()); + expect(messagePrompt!.commandPrompts[1].command).to.not.be.equal(undefined); + expect(messagePrompt!.commandPrompts[2].prompt).to.be.equal(ExtensionSurveyBanner.bannerLabelNo()); + expect(messagePrompt!.commandPrompts[2].command).to.not.be.equal(undefined); + }); + + test('Should return empty diagnostics if the diagnostic code has been ignored', async () => { + const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.MPLSSurveyDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + messageHandler.setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + }); + + test('MPLSSurveyDiagnosticService can handle MPLSSurveyDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.MPLSSurveyDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('MPLSSurveyDiagnosticService cannot handle non-MPLSSurveyDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('Should open brower with info on yes', async () => { + const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + + platformService + .setup((p) => p.osType) + .returns(() => OSType.Linux) + .verifiable(typemoq.Times.once()); + + appEnvironment + .setup((a) => a.vscodeVersion) + .returns(() => '1.56.2') + .verifiable(typemoq.Times.once()); + + appEnvironment + .setup((a) => a.packageJson) + .returns(() => ({ version: '2021.6.0' })) + .verifiable(typemoq.Times.once()); + + appEnvironment + .setup((a) => a.sessionId) + .returns(() => 'session-id') + .verifiable(typemoq.Times.once()); + + memento + .setup((m) => m.update(MPLS_SURVEY_MEMENTO, true)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + browserService + .setup((b) => + b.launch( + typemoq.It.isValue( + 'https://aka.ms/mpls-experience-survey?o=Linux&v=1.56.2&e=2021.6.0&m=session-id', + ), + ), + ) + .verifiable(typemoq.Times.once()); + + await messagePrompt!.commandPrompts[0].command!.invoke(); + + platformService.verifyAll(); + appEnvironment.verifyAll(); + browserService.verifyAll(); + memento.verifyAll(); + }); + + test('Should do nothing on later', async () => { + const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + + browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + memento.setup((m) => m.update(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await messagePrompt!.commandPrompts[1].command!.invoke(); + + platformService.verifyAll(); + appEnvironment.verifyAll(); + browserService.verifyAll(); + memento.verifyAll(); + }); + + test('Should do nothing on close', async () => { + const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + + browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + memento.setup((m) => m.update(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + messagePrompt!.onClose!(); + + platformService.verifyAll(); + appEnvironment.verifyAll(); + browserService.verifyAll(); + memento.verifyAll(); + }); + + test('Should update memento and not open browser on no', async () => { + const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + + browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + memento + .setup((m) => m.update(MPLS_SURVEY_MEMENTO, true)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await messagePrompt!.commandPrompts[2].command!.invoke(); + + platformService.verifyAll(); + appEnvironment.verifyAll(); + browserService.verifyAll(); + memento.verifyAll(); + }); +}); From d52a3090505a3acff6e352244b9cc39990024002 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 7 Jun 2021 14:39:16 -0700 Subject: [PATCH 3/3] Remove memento and other custom code --- .../diagnostics/checks/mplsSurvey.ts | 54 ++--- .../checks/mplsSurvey.unit.test.ts | 199 ++++++------------ 2 files changed, 83 insertions(+), 170 deletions(-) diff --git a/src/client/application/diagnostics/checks/mplsSurvey.ts b/src/client/application/diagnostics/checks/mplsSurvey.ts index 53073219e3d8..e6e95edf52bb 100644 --- a/src/client/application/diagnostics/checks/mplsSurvey.ts +++ b/src/client/application/diagnostics/checks/mplsSurvey.ts @@ -3,9 +3,9 @@ // eslint-disable-next-line max-classes-per-file import { inject, named } from 'inversify'; -import { DiagnosticSeverity, env, UIKind } from 'vscode'; +import { DiagnosticSeverity, UIKind } from 'vscode'; import * as querystring from 'querystring'; -import { IBrowserService, IDisposableRegistry, IExtensionContext, Resource } from '../../../common/types'; +import { IDisposableRegistry, Resource } from '../../../common/types'; import { ExtensionSurveyBanner } from '../../../common/utils/localize'; import { IServiceContainer } from '../../../ioc/types'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; @@ -14,8 +14,7 @@ import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '. import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; import { IApplicationEnvironment } from '../../../common/application/types'; import { IPlatformService } from '../../../common/platform/types'; - -export const MPLS_SURVEY_MEMENTO = 'mplsSurveyPromptMemento'; +import { IDiagnosticsCommandFactory } from '../commands/types'; export class MPLSSurveyDiagnostic extends BaseDiagnostic { constructor(message: string, resource: Resource) { @@ -32,24 +31,20 @@ export class MPLSSurveyDiagnostic extends BaseDiagnostic { export const MPLSSurveyDiagnosticServiceId = 'MPLSSurveyDiagnosticServiceId'; export class MPLSSurveyDiagnosticService extends BaseDiagnosticsService { - private disabledInCurrentSession = false; - constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IExtensionContext) private readonly context: IExtensionContext, @inject(IDiagnosticHandlerService) @named(DiagnosticCommandPromptHandlerServiceId) protected readonly messageService: IDiagnosticHandlerService, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment, @inject(IPlatformService) private platformService: IPlatformService, - @inject(IBrowserService) private browserService: IBrowserService, ) { super([DiagnosticCodes.MPLSSurveyDiagnostic], serviceContainer, disposableRegistry, true); } public async diagnose(resource: Resource): Promise { - if (!this.shouldShowPrompt) { + if (this.appEnvironment.uiKind === UIKind?.Web) { return []; } @@ -66,51 +61,32 @@ export class MPLSSurveyDiagnosticService extends BaseDiagnosticsService { return; } + const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + await this.messageService.handle(diagnostic, { commandPrompts: [ { prompt: ExtensionSurveyBanner.bannerLabelYes(), command: { diagnostic, - invoke: () => this.launchSurvey(), + invoke: () => this.launchSurvey(diagnostic), }, }, { prompt: ExtensionSurveyBanner.maybeLater(), - command: { - diagnostic, - invoke: async () => this.disable(), - }, }, { prompt: ExtensionSurveyBanner.bannerLabelNo(), - command: { - diagnostic, - invoke: () => this.updateMemento(), - }, + command: commandFactory.createCommand(diagnostic, { + type: 'ignore', + options: DiagnosticScope.Global, + }), }, ], - onClose: () => this.disable(), }); } - private async updateMemento() { - await this.context.globalState.update(MPLS_SURVEY_MEMENTO, true); - } - - private disable() { - this.disabledInCurrentSession = true; - } - - private get shouldShowPrompt(): boolean { - return ( - env.uiKind !== UIKind?.Web && - !this.disabledInCurrentSession && - !this.context.globalState.get(MPLS_SURVEY_MEMENTO) - ); - } - - private async launchSurvey() { + private async launchSurvey(diagnostic: IDiagnostic) { const query = querystring.stringify({ o: encodeURIComponent(this.platformService.osType), // platform v: encodeURIComponent(this.appEnvironment.vscodeVersion), @@ -118,7 +94,9 @@ export class MPLSSurveyDiagnosticService extends BaseDiagnosticsService { m: encodeURIComponent(this.appEnvironment.sessionId), }); const url = `https://aka.ms/mpls-experience-survey?${query}`; - this.browserService.launch(url); - await this.updateMemento(); + + const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + await commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }).invoke(); + await commandFactory.createCommand(diagnostic, { type: 'launch', options: url }).invoke(); } } diff --git a/src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts b/src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts index f3a347f02258..9d8f3e53212e 100644 --- a/src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts +++ b/src/test/application/diagnostics/checks/mplsSurvey.unit.test.ts @@ -5,24 +5,25 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; -import { ExtensionContext } from 'vscode'; +import { UIKind } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; import { MPLSSurveyDiagnostic, MPLSSurveyDiagnosticService, - MPLS_SURVEY_MEMENTO, } from '../../../../client/application/diagnostics/checks/mplsSurvey'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; import { + DiagnosticScope, IDiagnostic, + IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService, } from '../../../../client/application/diagnostics/types'; import { IApplicationEnvironment } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; -import { IBrowserService, IExtensionContext } from '../../../../client/common/types'; import { ExtensionSurveyBanner } from '../../../../client/common/utils/localize'; import { OSType } from '../../../../client/common/utils/platform'; import { IServiceContainer } from '../../../../client/ioc/types'; @@ -30,28 +31,26 @@ import { IServiceContainer } from '../../../../client/ioc/types'; suite('Application Diagnostics - MPLS survey', () => { let serviceContainer: typemoq.IMock; let diagnosticService: IDiagnosticsService; + let commandFactory: typemoq.IMock; let filterService: typemoq.IMock; let messageHandler: typemoq.IMock>; - let context: typemoq.IMock; - let memento: typemoq.IMock; let appEnvironment: typemoq.IMock; let platformService: typemoq.IMock; - let browserService: typemoq.IMock; setup(() => { serviceContainer = typemoq.Mock.ofType(); filterService = typemoq.Mock.ofType(); messageHandler = typemoq.Mock.ofType>(); - context = typemoq.Mock.ofType(); - memento = typemoq.Mock.ofType(); appEnvironment = typemoq.Mock.ofType(); platformService = typemoq.Mock.ofType(); - browserService = typemoq.Mock.ofType(); + commandFactory = typemoq.Mock.ofType(); serviceContainer .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) .returns(() => filterService.object); - context.setup((c) => c.globalState).returns(() => memento.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); diagnosticService = new (class extends MPLSSurveyDiagnosticService { // eslint-disable-next-line class-methods-use-this @@ -60,27 +59,13 @@ suite('Application Diagnostics - MPLS survey', () => { BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); } } - })( - serviceContainer.object, - context.object, - messageHandler.object, - [], - appEnvironment.object, - platformService.object, - browserService.object, - ); + })(serviceContainer.object, messageHandler.object, [], appEnvironment.object, platformService.object); // eslint-disable-next-line @typescript-eslint/no-explicit-any (diagnosticService as any)._clear(); }); - teardown(() => { - context.reset(); - memento.reset(); - }); - - test('Should display message the prompt has not been shown yet', async () => { - memento.setup((m) => m.get(MPLS_SURVEY_MEMENTO)).returns(() => undefined); - + test('Should diagnose survey', async () => { + appEnvironment.setup((a) => a.uiKind).returns(() => UIKind.Desktop); const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.deep.equal([ @@ -88,8 +73,8 @@ suite('Application Diagnostics - MPLS survey', () => { ]); }); - test('Should return empty diagnostics if the prompt has been shown before', async () => { - memento.setup((m) => m.get(MPLS_SURVEY_MEMENTO)).returns(() => true); + test('Should not diagnose if in web UI', async () => { + appEnvironment.setup((a) => a.uiKind).returns(() => UIKind.Web); const diagnostics = await diagnosticService.diagnose(undefined); @@ -108,24 +93,39 @@ suite('Application Diagnostics - MPLS survey', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + const alwaysIgnoreCommand = typemoq.Mock.ofType(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith>({ + type: 'ignore', + options: DiagnosticScope.Global, + }), + ), + ) + .returns(() => alwaysIgnoreCommand.object) + .verifiable(typemoq.Times.once()); + + alwaysIgnoreCommand.setup((c) => c.invoke()).verifiable(typemoq.Times.never()); await diagnosticService.handle([diagnostic]); filterService.verifyAll(); messageHandler.verifyAll(); - browserService.verifyAll(); + commandFactory.verifyAll(); + alwaysIgnoreCommand.verifyAll(); expect(messagePrompt).to.not.be.equal(undefined); - expect(messagePrompt!.onClose).to.not.be.equal(undefined); + expect(messagePrompt!.onClose).to.be.equal(undefined, 'onClose was not undefined'); expect(messagePrompt!.commandPrompts).to.be.lengthOf(3); expect(messagePrompt!.commandPrompts[0].prompt).to.be.equal(ExtensionSurveyBanner.bannerLabelYes()); - expect(messagePrompt!.commandPrompts[0].command).to.not.be.equal(undefined); + expect(messagePrompt!.commandPrompts[0].command).to.not.be.equal(undefined, 'Yes command was undefined'); expect(messagePrompt!.commandPrompts[1].prompt).to.be.equal(ExtensionSurveyBanner.maybeLater()); - expect(messagePrompt!.commandPrompts[1].command).to.not.be.equal(undefined); + expect(messagePrompt!.commandPrompts[1].command).to.be.equal(undefined, 'Later command was not undefined'); expect(messagePrompt!.commandPrompts[2].prompt).to.be.equal(ExtensionSurveyBanner.bannerLabelNo()); - expect(messagePrompt!.commandPrompts[2].command).to.not.be.equal(undefined); + expect(messagePrompt!.commandPrompts[2].command).to.be.equal(alwaysIgnoreCommand.object); }); test('Should return empty diagnostics if the diagnostic code has been ignored', async () => { @@ -182,10 +182,24 @@ suite('Application Diagnostics - MPLS survey', () => { .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await diagnosticService.handle([diagnostic]); + const alwaysIgnoreCommand = typemoq.Mock.ofType(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith>({ + type: 'ignore', + options: DiagnosticScope.Global, + }), + ), + ) + .returns(() => alwaysIgnoreCommand.object) + .verifiable(typemoq.Times.once()); - filterService.verifyAll(); - messageHandler.verifyAll(); + alwaysIgnoreCommand + .setup((c) => c.invoke()) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); platformService .setup((p) => p.osType) @@ -207,94 +221,22 @@ suite('Application Diagnostics - MPLS survey', () => { .returns(() => 'session-id') .verifiable(typemoq.Times.once()); - memento - .setup((m) => m.update(MPLS_SURVEY_MEMENTO, true)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - browserService - .setup((b) => - b.launch( - typemoq.It.isValue( - 'https://aka.ms/mpls-experience-survey?o=Linux&v=1.56.2&e=2021.6.0&m=session-id', - ), + const launchCommand = typemoq.Mock.ofType(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith>({ + type: 'launch', + options: 'https://aka.ms/mpls-experience-survey?o=Linux&v=1.56.2&e=2021.6.0&m=session-id', + }), ), ) + .returns(() => launchCommand.object) .verifiable(typemoq.Times.once()); - await messagePrompt!.commandPrompts[0].command!.invoke(); - - platformService.verifyAll(); - appEnvironment.verifyAll(); - browserService.verifyAll(); - memento.verifyAll(); - }); - - test('Should do nothing on later', async () => { - const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); - let messagePrompt: MessageCommandPrompt | undefined; - - messageHandler - .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((_d, prompt: MessageCommandPrompt) => { - messagePrompt = prompt; - }) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic]); - - filterService.verifyAll(); - messageHandler.verifyAll(); - - browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); - memento.setup((m) => m.update(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); - - await messagePrompt!.commandPrompts[1].command!.invoke(); - - platformService.verifyAll(); - appEnvironment.verifyAll(); - browserService.verifyAll(); - memento.verifyAll(); - }); - - test('Should do nothing on close', async () => { - const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); - let messagePrompt: MessageCommandPrompt | undefined; - - messageHandler - .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((_d, prompt: MessageCommandPrompt) => { - messagePrompt = prompt; - }) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic]); - - filterService.verifyAll(); - messageHandler.verifyAll(); - - browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); - memento.setup((m) => m.update(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); - - messagePrompt!.onClose!(); - - platformService.verifyAll(); - appEnvironment.verifyAll(); - browserService.verifyAll(); - memento.verifyAll(); - }); - - test('Should update memento and not open browser on no', async () => { - const diagnostic = new MPLSSurveyDiagnostic(DiagnosticCodes.MPLSSurveyDiagnostic, undefined); - let messagePrompt: MessageCommandPrompt | undefined; - - messageHandler - .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((_d, prompt: MessageCommandPrompt) => { - messagePrompt = prompt; - }) + launchCommand + .setup((c) => c.invoke()) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); @@ -303,18 +245,11 @@ suite('Application Diagnostics - MPLS survey', () => { filterService.verifyAll(); messageHandler.verifyAll(); - browserService.setup((b) => b.launch(typemoq.It.isAny())).verifiable(typemoq.Times.never()); - - memento - .setup((m) => m.update(MPLS_SURVEY_MEMENTO, true)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - await messagePrompt!.commandPrompts[2].command!.invoke(); + await messagePrompt!.commandPrompts[0].command!.invoke(); platformService.verifyAll(); appEnvironment.verifyAll(); - browserService.verifyAll(); - memento.verifyAll(); + alwaysIgnoreCommand.verifyAll(); + launchCommand.verifyAll(); }); });