diff --git a/packages/app/client/src/commands/botCommands.spec.ts b/packages/app/client/src/commands/botCommands.spec.ts
index e2643a759..c5c909dcf 100644
--- a/packages/app/client/src/commands/botCommands.spec.ts
+++ b/packages/app/client/src/commands/botCommands.spec.ts
@@ -87,12 +87,20 @@ describe('The bot commands', () => {
});
it('should make the appropriate calls to switch bots', () => {
+ const remoteCallArgs = [];
+ CommandServiceImpl.remoteCall = async (...args: any[]) => {
+ remoteCallArgs.push(args);
+ return true;
+ };
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots');
const { handler } = registry.getCommand(
SharedConstants.Commands.Bot.Switch
);
handler({});
expect(spy).toHaveBeenCalledWith({});
+ expect(remoteCallArgs[0][0]).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
+ expect(remoteCallArgs[0][1]).toBe('bot_open');
+ expect(remoteCallArgs[0][2]).toEqual({ method: 'bots_list', numOfServices: undefined });
});
it('should make the appropriate calls to close a bot', () => {
diff --git a/packages/app/client/src/commands/emulatorCommands.spec.ts b/packages/app/client/src/commands/emulatorCommands.spec.ts
index 1d518391d..e2c62d4c4 100644
--- a/packages/app/client/src/commands/emulatorCommands.spec.ts
+++ b/packages/app/client/src/commands/emulatorCommands.spec.ts
@@ -140,6 +140,11 @@ describe('The emulator commands', () => {
title: 'Open transcript file',
}
);
+ expect(remoteCallSpy).toHaveBeenCalledWith(
+ SharedConstants.Commands.Telemetry.TrackEvent,
+ 'transcriptFile_open',
+ { method: 'file_menu' }
+ );
expect(callSpy).toHaveBeenCalledWith(
'transcript:open',
diff --git a/packages/app/client/src/commands/uiCommands.spec.ts b/packages/app/client/src/commands/uiCommands.spec.ts
index 0fbb1dd42..62ecfb396 100644
--- a/packages/app/client/src/commands/uiCommands.spec.ts
+++ b/packages/app/client/src/commands/uiCommands.spec.ts
@@ -32,7 +32,6 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
-
import {
CONTENT_TYPE_APP_SETTINGS,
DOCUMENT_ID_APP_SETTINGS,
@@ -57,8 +56,9 @@ import {
OpenBotDialogContainer,
SecretPromptDialogContainer,
} from '../ui/dialogs';
-
import { registerCommands } from './uiCommands';
+import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
+
jest.mock('../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: class {},
AzureLoginSuccessDialogContainer: class {},
@@ -153,10 +153,16 @@ describe('the uiCommands', () => {
});
it('should set the proper href on the theme tag when the SwitchTheme command is dispatched', () => {
- const link = document.createElement('link');
+ const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
+ let link = document.createElement('link');
link.id = 'themeVars';
document.querySelector('head').appendChild(link);
registry.getCommand(Commands.SwitchTheme).handler('light', './light.css');
expect(link.href).toBe('http://localhost/light.css');
+ expect(remoteCallSpy).toHaveBeenCalledWith(
+ SharedConstants.Commands.Telemetry.TrackEvent,
+ 'app_chooseTheme',
+ { themeName: 'light' }
+ );
});
});
diff --git a/packages/app/client/src/data/sagas/azureAuthSaga.spec.ts b/packages/app/client/src/data/sagas/azureAuthSaga.spec.ts
index 756805842..8cc9e195e 100644
--- a/packages/app/client/src/data/sagas/azureAuthSaga.spec.ts
+++ b/packages/app/client/src/data/sagas/azureAuthSaga.spec.ts
@@ -182,6 +182,7 @@ describe('The azureAuthSaga', () => {
ct++;
}
expect(ct).toBe(5);
+ expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Telemetry.TrackEvent, 'signIn_failure');
});
it('should contain 6 steps when the Azure login dialog prompt is confirmed and auth succeeds', async () => {
@@ -257,6 +258,10 @@ describe('The azureAuthSaga', () => {
expect(store.getState().azureAuth.access_token).toBe(
'a valid access_token'
);
+ expect(remoteCallSpy).toHaveBeenCalledWith(
+ SharedConstants.Commands.Telemetry.TrackEvent,
+ 'signIn_success'
+ );
});
});
});
diff --git a/packages/app/client/src/data/sagas/resourceSagas.spec.ts b/packages/app/client/src/data/sagas/resourceSagas.spec.ts
index 49f6b61b4..2fc706041 100644
--- a/packages/app/client/src/data/sagas/resourceSagas.spec.ts
+++ b/packages/app/client/src/data/sagas/resourceSagas.spec.ts
@@ -241,7 +241,7 @@ describe('The ResourceSagas', () => {
});
});
- describe(',when opening the resource in the Emulator', () => {
+ describe('when opening the resource in the Emulator', () => {
let mockResource;
beforeEach(() => {
mockResource = BotConfigWithPathImpl.serviceFromJSON({
@@ -256,9 +256,15 @@ describe('The ResourceSagas', () => {
expect(mockLocalCommandsCalled).toEqual([
{
commandName: 'chat:open',
- args: ['the/file/path/chat.chat', true],
- },
+ args: ['the/file/path/chat.chat', true]
+ }
]);
+ expect(mockRemoteCommandsCalled).toEqual([
+ {
+ commandName: SharedConstants.Commands.Telemetry.TrackEvent,
+ args: ['chatFile_open']
+ }
+ ])
});
it('should open a transcript file', async () => {
@@ -267,9 +273,14 @@ describe('The ResourceSagas', () => {
expect(mockLocalCommandsCalled).toEqual([
{
commandName: 'transcript:open',
- args: ['the/file/path/transcript.transcript'],
- },
+ args: ['the/file/path/transcript.transcript']
+ }
]);
+ expect(mockRemoteCommandsCalled).toEqual([
+ {
+ commandName: SharedConstants.Commands.Telemetry.TrackEvent,
+ args: ['transcriptFile_open', { method: 'resources_pane' }]
+ }])
});
});
diff --git a/packages/app/client/src/ui/editor/emulator/emulator.spec.tsx b/packages/app/client/src/ui/editor/emulator/emulator.spec.tsx
new file mode 100644
index 000000000..d8baebb56
--- /dev/null
+++ b/packages/app/client/src/ui/editor/emulator/emulator.spec.tsx
@@ -0,0 +1,394 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Bot Framework: http://botframework.com
+//
+// Bot Framework Emulator Github:
+// https://github.com/Microsoft/BotFramwork-Emulator
+//
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License:
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import * as React from 'react';
+import { Provider } from 'react-redux';
+import { createStore } from 'redux';
+import { mount, shallow } from 'enzyme';
+import { Emulator, EmulatorComponent, RestartConversationOptions } from './emulator';
+import { disable, enable } from '../../../data/action/presentationActions';
+import {
+ clearLog,
+ newConversation,
+ setInspectorObjects,
+ updateChat
+} from '../../../data/action/chatActions';
+import { updateDocument } from '../../../data/action/editorActions';
+import { SharedConstants } from '@bfemulator/app-shared';
+import base64Url from 'base64url';
+const { encode } = base64Url;
+
+let mockCallsMade, mockRemoteCallsMade;
+let mockSharedConstants = SharedConstants;
+jest.mock('../../../platform/commands/commandServiceImpl', () => ({
+ CommandServiceImpl: {
+ call: (commandName, ...args) => {
+ mockCallsMade.push({ commandName, args });
+ return Promise.resolve();
+ },
+ remoteCall: (commandName, ...args) => {
+ mockRemoteCallsMade.push({ commandName, args });
+ if (commandName === mockSharedConstants.Commands.Emulator.NewTranscript) {
+ return Promise.resolve({ conversationId: 'someConvoId' });
+ }
+ if (commandName === mockSharedConstants.Commands.Emulator.FeedTranscriptFromDisk) {
+ return Promise.resolve({ meta: 'some file info' });
+ }
+ return Promise.resolve();
+ },
+ }
+}));
+jest.mock('./chatPanel/chatPanel', () => {
+ return jest.fn(() =>
);
+});
+jest.mock('./logPanel/logPanel', () => {
+ return jest.fn(() => );
+});
+jest.mock('./playbackBar/playbackBar', () => {
+ return jest.fn(() => );
+});
+jest.mock('./emulator.scss', () => ({}));
+jest.mock('./parts', () => {
+ return jest.fn(() => );
+});
+jest.mock('./toolbar/toolbar', () => {
+ return jest.fn(() => );
+});
+jest.mock('@bfemulator/sdk-shared', () => ({
+ uniqueId: () => 'someUniqueId',
+ uniqueIdv4: () => 'newUserId'
+}));
+
+jest.mock('botframework-webchat', () => ({
+ createDirectLine: (...args) => ({ args })
+}));
+
+describe('', () => {
+ let wrapper;
+ let node;
+ let instance;
+ let mockDispatch;
+ let mockStoreState;
+
+ beforeEach(() => {
+ mockCallsMade = [];
+ mockRemoteCallsMade = [];
+ mockStoreState = {
+ chat: {
+ chats: {
+ doc1: {
+ conversationId: 'convo1',
+ documentId: 'doc1',
+ endpointId: 'endpoint1'
+ }
+ }
+ },
+ editor: {
+ activeEditor: 'primary',
+ editors: {
+ primary: {
+ activeDocumentId: 'doc1'
+ }
+ }
+ },
+ presentation: { enabled: true }
+ };
+ const mockStore = createStore((_state, _action) => mockStoreState);
+ mockDispatch = jest.spyOn(mockStore, 'dispatch');
+ wrapper = mount(
+
+
+
+ );
+ node = wrapper.find(EmulatorComponent);
+ instance = node.instance();
+ });
+
+ it('should render properly', () => {
+ expect(instance).not.toBe(true);
+ });
+
+ it('should determine when to start a new conversation', () => {
+ expect(instance.shouldStartNewConversation()).toBe(true);
+
+ mockStoreState.chat.chats.doc1.directLine = { conversationId: 'convo2' };
+ expect(instance.shouldStartNewConversation()).toBe(true);
+
+ mockStoreState.chat.chats.doc1.directLine = { conversationId: 'convo1' };
+ expect(instance.shouldStartNewConversation()).toBe(false);
+ });
+
+ it('should render the presentation view', () => {
+ wrapper = shallow(
+ null) }
+ newConversation={ jest.fn(() => null) }
+ mode={ 'transcript' }
+ document={ mockStoreState.chat.chats.doc1 }/>
+ );
+ instance = wrapper.instance();
+ const presentationView = instance.renderPresentationView();
+
+ expect(presentationView).not.toBeNull();
+ });
+
+ it('should render the default view', () => {
+ wrapper = shallow(
+ null) }
+ newConversation={ jest.fn(() => null) }
+ mode={ 'transcript' }
+ document={ mockStoreState.chat.chats.doc1 }/>
+ );
+ instance = wrapper.instance();
+ const defaultView = instance.renderDefaultView();
+
+ expect(defaultView).not.toBeNull();
+ });
+
+ it('should get the veritcal splitter sizes', () => {
+ mockStoreState.chat.chats.doc1.ui = {
+ verticalSplitter: {
+ 0: {
+ percentage: '55'
+ }
+ }
+ };
+ wrapper = shallow(
+ null) }
+ newConversation={ jest.fn(() => null) }
+ mode={ 'transcript' }
+ document={ mockStoreState.chat.chats.doc1 }/>
+ );
+ instance = wrapper.instance();
+ const verticalSplitterSizes = instance.getVerticalSplitterSizes();
+
+ expect(verticalSplitterSizes[0]).toBe('55');
+ });
+
+ it('should get the veritcal splitter sizes', () => {
+ mockStoreState.chat.chats.doc1.ui = {
+ horizontalSplitter: {
+ 0: {
+ percentage: '46'
+ }
+ }
+ };
+ wrapper = shallow(
+ null) }
+ newConversation={ jest.fn(() => null) }
+ mode={ 'transcript' }
+ document={ mockStoreState.chat.chats.doc1 }/>
+ );
+ instance = wrapper.instance();
+ const horizontalSplitterSizes = instance.getHorizontalSplitterSizes();
+
+ expect(horizontalSplitterSizes[0]).toBe('46');
+ });
+
+ it('should restart the conversation on Ctrl/Cmd + Shift + R', () => {
+ wrapper = shallow(
+ null) }
+ newConversation={ jest.fn(() => null) }
+ mode={ 'transcript' }
+ document={ mockStoreState.chat.chats.doc1 }/>
+ );
+ instance = wrapper.instance();
+ const mockOnStartOverClick = jest.fn(() => null);
+ instance.onStartOverClick = mockOnStartOverClick;
+ let mockGetModifierState = jest.fn(modifier => {
+ if (modifier === 'Control') {
+ return true;
+ } else if (modifier === 'Shift') {
+ return true;
+ }
+ return true;
+ });
+ let mockEvent = {
+ getModifierState: mockGetModifierState,
+ key: 'R'
+ };
+ instance.keyboardEventListener(mockEvent);
+
+ expect(mockOnStartOverClick).toHaveBeenCalledTimes(1);
+
+ mockGetModifierState = jest.fn(modifier => {
+ if (modifier === 'Control') {
+ return false;
+ } else if (modifier === 'Shift') {
+ return true;
+ } else {
+ return true; // Cmd / Meta
+ }
+ });
+ instance.keyboardEventListener(mockEvent);
+
+ expect(mockOnStartOverClick).toHaveBeenCalledTimes(2);
+ });
+
+ it('should enable presentation mode', () => {
+ instance.onPresentationClick(true);
+
+ expect(mockDispatch).toHaveBeenCalledWith(enable());
+ });
+
+ it('should disable presentation mode', () => {
+ instance.onPresentationClick(false);
+
+ expect(mockDispatch).toHaveBeenCalledWith(disable());
+ });
+
+ it('should export a transcript', () => {
+ mockStoreState.chat.chats.doc1.directLine = {
+ conversationId: 'convo1'
+ };
+ wrapper = shallow(
+ null) }
+ newConversation={ jest.fn(() => null) }
+ mode={ 'transcript' }
+ document={ mockStoreState.chat.chats.doc1 }/>
+ );
+ instance = wrapper.instance();
+ instance.onExportClick();
+
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.SaveTranscriptToFile);
+ expect(mockRemoteCallsMade[0].args).toEqual(['convo1']);
+ });
+
+ it('should start over a conversation with a new user id', async () => {
+ await instance.onStartOverClick();
+
+ expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
+ expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
+ expect(mockDispatch).toHaveBeenCalledWith(updateChat('doc1', { userId: 'newUserId' }));
+ expect(mockRemoteCallsMade).toHaveLength(2);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
+ expect(mockRemoteCallsMade[0].args).toEqual(['conversation_restart', { userId: 'new' }]);
+ expect(mockRemoteCallsMade[1].commandName).toBe(SharedConstants.Commands.Emulator.SetCurrentUser);
+ expect(mockRemoteCallsMade[1].args).toEqual(['newUserId']);
+ });
+
+ it('should start over a conversation with the same user id', async () => {
+ const mockStartNewConversation = jest.fn(() => null);
+ instance.startNewConversation = mockStartNewConversation;
+
+ await instance.onStartOverClick(RestartConversationOptions.SameUserId);
+
+ expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
+ expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
+ expect(mockRemoteCallsMade[0].args).toEqual(['conversation_restart', { userId: 'same' }]);
+ expect(mockStartNewConversation).toHaveBeenCalledTimes(1);
+ });
+
+ it('should init a conversation', () => {
+ const mockProps = {
+ documentId: 'doc1',
+ url: 'someUrl'
+ };
+ const mockOptions = { conversationId: 'convo1' };
+ const encodedOptions = encode(JSON.stringify(mockOptions));
+ instance.initConversation(mockProps, mockOptions, {}, {});
+
+ expect(mockDispatch).toHaveBeenCalledWith(
+ newConversation(
+ 'doc1',
+ {
+ conversationId: 'convo1',
+ directLine: {
+ args: [{
+ secret: encodedOptions,
+ domain: 'someUrl/v3/directline',
+ webSocket: false
+ }]
+ },
+ selectedActivity$: {},
+ subscription: {}
+ }
+ )
+ );
+ });
+
+ it('should start a new conversation from transcript in memory', async () => {
+ const mockInitConversation = jest.fn(() => null);
+ instance.initConversation = mockInitConversation;
+ let mockProps = {
+ document: {
+ activities: [],
+ botId: 'someBotId',
+ inMemory: true,
+ userId: 'someUserId'
+ },
+ mode: 'transcript'
+ };
+
+ await instance.startNewConversation(mockProps);
+
+ expect(mockRemoteCallsMade).toHaveLength(2);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.NewTranscript);
+ expect(mockRemoteCallsMade[0].args).toEqual(['someUniqueId|transcript']);
+ expect(mockRemoteCallsMade[1].commandName).toBe(SharedConstants.Commands.Emulator.FeedTranscriptFromMemory);
+ expect(mockRemoteCallsMade[1].args).toEqual(['someConvoId', 'someBotId', 'someUserId', []]);
+ });
+
+ it('should start a new conversation from transcript on disk', async () => {
+ const mockInitConversation = jest.fn(() => null);
+ instance.initConversation = mockInitConversation;
+ let mockProps = {
+ document: {
+ activities: [],
+ botId: 'someBotId',
+ documentId: 'someDocId',
+ inMemory: false,
+ userId: 'someUserId'
+ },
+ documentId: 'someDocId',
+ mode: 'transcript'
+ };
+
+ await instance.startNewConversation(mockProps);
+
+ expect(mockRemoteCallsMade).toHaveLength(2);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.NewTranscript);
+ expect(mockRemoteCallsMade[0].args).toEqual(['someUniqueId|transcript']);
+ expect(mockRemoteCallsMade[1].commandName).toBe(SharedConstants.Commands.Emulator.FeedTranscriptFromDisk);
+ expect(mockRemoteCallsMade[1].args).toEqual(['someConvoId', 'someBotId', 'someUserId', 'someDocId']);
+ expect(mockDispatch).toHaveBeenCalledWith(updateDocument('someDocId', { meta: 'some file info' }));
+ });
+});
diff --git a/packages/app/client/src/ui/editor/emulator/emulator.tsx b/packages/app/client/src/ui/editor/emulator/emulator.tsx
index ab3e49c06..58297d938 100644
--- a/packages/app/client/src/ui/editor/emulator/emulator.tsx
+++ b/packages/app/client/src/ui/editor/emulator/emulator.tsx
@@ -63,7 +63,7 @@ import { ToolBar } from './toolbar/toolbar';
const { encode } = base64Url;
-const RestartConversationOptions = {
+export const RestartConversationOptions = {
NewUserId: 'Restart with new user ID',
SameUserId: 'Restart with same user ID',
};
@@ -93,7 +93,7 @@ interface EmulatorProps {
url?: string;
}
-class EmulatorComponent extends React.Component {
+export class EmulatorComponent extends React.Component {
private readonly onVerticalSizeChange = debounce(sizes => {
this.props.document.ui = {
...this.props.document.ui,
@@ -227,7 +227,7 @@ class EmulatorComponent extends React.Component {
props.document.documentId
);
- this.props.updateDocument(this.props.documentId, fileInfo);
+ this.props.updateDocument(props.documentId, fileInfo);
} catch (err) {
throw new Error(
`Error while feeding transcript on disk to conversation: ${err}`
diff --git a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx
index facf72674..57374d770 100644
--- a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx
+++ b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx
@@ -30,6 +30,7 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
+
import * as React from 'react';
import { Provider } from 'react-redux';
import { mount } from 'enzyme';
@@ -39,7 +40,6 @@ import {
textItem,
} from '@bfemulator/emulator-core/lib/types/log/util';
import LogLevel from '@bfemulator/emulator-core/lib/types/log/level';
-
import { bot } from '../../../../../data/reducer/bot';
import { clientAwareSettings } from '../../../../../data/reducer/clientAwareSettingsReducer';
import { load, setActive } from '../../../../../data/action/botActions';
@@ -47,7 +47,7 @@ import { theme } from '../../../../../data/reducer/themeReducer';
import { switchTheme } from '../../../../../data/action/themeActions';
import { ExtensionManager } from '../../../../../extensions';
import { LogService } from '../../../../../platform/log/logService';
-
+import { SharedConstants } from '@bfemulator/app-shared';
import { InspectorContainer } from './inspectorContainer';
import { Inspector } from './inspector';
@@ -64,6 +64,15 @@ jest.mock('../../../../../data/store', () => ({
},
}));
+let mockRemoteCallsMade;
+jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
+ CommandServiceImpl: {
+ remoteCall: (commandName, ...args) => {
+ mockRemoteCallsMade.push({ commandName, args });
+ }
+ }
+}));
+
const mockState = {
bot: {
description: '',
@@ -268,6 +277,7 @@ describe('The Inspector component', () => {
mockStore.dispatch(switchTheme('light', ['vars.css', 'light.css']));
mockStore.dispatch(load([mockState.bot]));
mockStore.dispatch(setActive(mockState.bot as any));
+ mockRemoteCallsMade = [];
parent = mount(
@@ -384,5 +394,26 @@ describe('The Inspector component', () => {
logEntry(textItem(LogLevel.Info, text))
);
});
+
+ it('"track-event"', () => {
+ event.channel = 'track-event';
+ event.args[0] = 'someEvent';
+ event.args[1] = { some: 'data' };
+ instance.ipcMessageEventHandler(event);
+
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0]).toEqual({
+ commandName: SharedConstants.Commands.Telemetry.TrackEvent,
+ args: ['someEvent', { some: 'data' }]
+ });
+
+ event.args[1] = undefined;
+ instance.ipcMessageEventHandler(event);
+ expect(mockRemoteCallsMade).toHaveLength(2);
+ expect(mockRemoteCallsMade[1]).toEqual({
+ commandName: SharedConstants.Commands.Telemetry.TrackEvent,
+ args: ['someEvent', {}]
+ });
+ });
});
});
diff --git a/packages/app/client/src/ui/editor/emulator/parts/log/logEntry.spec.tsx b/packages/app/client/src/ui/editor/emulator/parts/log/logEntry.spec.tsx
index 0bea83418..7054d8333 100644
--- a/packages/app/client/src/ui/editor/emulator/parts/log/logEntry.spec.tsx
+++ b/packages/app/client/src/ui/editor/emulator/parts/log/logEntry.spec.tsx
@@ -33,29 +33,81 @@
import * as React from 'react';
import { mount, ReactWrapper } from 'enzyme';
+import { Provider } from 'react-redux';
+import { createStore } from 'redux';
import LogLevel from '@bfemulator/emulator-core/lib/types/log/level';
import { textItem } from '@bfemulator/emulator-core/lib/types/log/util';
+import {
+ number2,
+ timestamp,
+ LogEntry,
+ LogEntryProps
+} from './logEntry';
+import { LogEntry as LogEntryContainer } from './logEntryContainer';
+import { SharedConstants } from '@bfemulator/app-shared';
+import { setInspectorObjects } from '../../../../../data/action/chatActions';
-import { number2, timestamp, LogEntry, LogEntryProps } from './logEntry';
+jest.mock('../../../../dialogs', () => ({
+ BotCreationDialog: () => ({})
+}));
jest.mock('./log.scss', () => ({}));
+let mockRemoteCallsMade;
+let mockCallsMade;
+jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
+ CommandServiceImpl: {
+ call: (commandName, ...args) => {
+ mockCallsMade.push({ commandName, args });
+ return Promise.resolve(true);
+ },
+ remoteCall: (commandName, ...args) => {
+ mockRemoteCallsMade.push({ commandName, args });
+ return Promise.resolve(true);
+ }
+ }
+}));
+
describe('logEntry component', () => {
let wrapper: ReactWrapper;
+ let node;
+ let instance;
+ let props: LogEntryProps;
+ let mockNext;
+ let mockSelectedActivity;
+ let mockSetInspectorObjects;
+ let mockDispatch;
beforeEach(() => {
- const props: LogEntryProps = {
- document: {},
+ mockNext = jest.fn(() => null);
+ mockSelectedActivity = { next: mockNext };
+ mockSetInspectorObjects = jest.fn(() => null);
+ mockRemoteCallsMade = [];
+ mockCallsMade = [];
+ props = {
+ document: {
+ documentId: 'someDocId',
+ selectedActivity$: mockSelectedActivity
+ },
entry: {
timestamp: 0,
- items: [],
+ items: []
},
+ setInspectorObjects: mockSetInspectorObjects
};
- wrapper = mount();
+ const mockStore = createStore((_state, _action) => ({}));
+ mockDispatch = jest.spyOn(mockStore, 'dispatch');
+ wrapper = mount(
+
+
+
+ );
+ node = wrapper.find(LogEntry);
+ instance = node.instance();
});
it('should render an outer entry component', () => {
- expect(wrapper.find('div')).toHaveLength(1);
+ expect(node.find('div')).toHaveLength(1);
});
it('should render a timestamped log entry with multiple items', () => {
@@ -67,6 +119,7 @@ describe('logEntry component', () => {
textItem(LogLevel.Debug, 'item3'),
],
};
+ wrapper = mount();
wrapper.setProps({ entry });
expect(wrapper.find('span.timestamp')).toHaveLength(1);
expect(wrapper.find('span.text-item')).toHaveLength(3);
@@ -75,7 +128,7 @@ describe('logEntry component', () => {
expect(timestampNode.html()).toContain('12:34:56');
});
- test('number2', () => {
+ it('should truncate a number of more than 3 digits to 2 digits', () => {
const num1 = 5;
const num2 = 34;
const num3 = 666;
@@ -85,7 +138,7 @@ describe('logEntry component', () => {
expect(number2(num3)).toBe('66');
});
- test('timestamp', () => {
+ it('should properly generate a timestamp', () => {
const time = Date.now();
const date = new Date(time);
const expectedHrs = number2(date.getHours());
@@ -95,4 +148,149 @@ describe('logEntry component', () => {
expect(timestamp(time)).toBe(expectedTimestamp);
});
+
+ it('should inspect an object', () => {
+ const mockInspectableObj = { some: 'data' };
+ instance.inspect(mockInspectableObj);
+
+ expect(mockNext).toHaveBeenCalledWith({ showInInspector: true });
+ expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('someDocId', mockInspectableObj));
+ });
+
+ it('should inspect and highlight an object', () => {
+ const mockInspectableObj = { some: 'data', type: 'message', id: 'someId' };
+ instance.inspectAndHighlightInWebchat(mockInspectableObj);
+
+ expect(mockNext).toHaveBeenCalledWith({ ...mockInspectableObj, showInInspector: true });
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
+ expect(mockRemoteCallsMade[0].args).toEqual(['log_inspectActivity', { type: 'message' }]);
+
+ mockInspectableObj.type = undefined;
+ instance.inspectAndHighlightInWebchat(mockInspectableObj);
+
+ expect(mockRemoteCallsMade[1].args).toEqual(['log_inspectActivity', { type: '' }]);
+ });
+
+ it('should highlight an object', () => {
+ const mockInspectableObj = { some: 'data', type: 'message', id: 'someId' };
+ instance.highlightInWebchat(mockInspectableObj);
+
+ expect(mockNext).toHaveBeenCalledWith({ ...mockInspectableObj, showInInspector: false });
+ });
+
+ it('should remove highlighting from an object', () => {
+ const mockInspectableObj = { id: 'activity1' };
+ wrapper = mount();
+ const mockCurrentlyInspectedActivity = { id: 'activity2' };
+ wrapper.setProps({ currentlyInspectedActivity: mockCurrentlyInspectedActivity });
+ instance = wrapper.instance();
+ instance.removeHighlightInWebchat(mockInspectableObj);
+
+ expect(mockNext).toHaveBeenCalledWith({ ...mockCurrentlyInspectedActivity, showInInspector: true });
+
+ mockCurrentlyInspectedActivity.id = undefined;
+ instance.removeHighlightInWebchat(mockInspectableObj);
+
+ expect(mockNext).toHaveBeenCalledWith({ showInInspector: false });
+ });
+
+ it('should render a text item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const textElem = instance.renderItem(
+ { type: 'text', payload: { level: LogLevel.Debug, text: 'some text' }},
+ 'someKey'
+ );
+ expect(textElem).not.toBeNull();
+ });
+
+ it('should render an external link item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const linkItem = instance.renderItem(
+ { type: 'external-link', payload: { hyperlink: 'https://aka.ms/bf-emulator', text: 'some text' }},
+ 'someKey'
+ );
+ expect(linkItem).not.toBeNull();
+ });
+
+ it('should render an app settings item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const appSettingsItem = instance.renderItem(
+ { type: 'open-app-settings', payload: { text: 'some text' }},
+ 'someKey'
+ );
+ expect(appSettingsItem).not.toBeNull();
+ });
+
+ it('should render an exception item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const exceptionItem = instance.renderItem(
+ { type: 'exception', payload: { err: 'some error' }},
+ 'someKey'
+ );
+ expect(exceptionItem).not.toBeNull();
+ });
+
+ it('should render an inspectable object item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const inspectableObjItem = instance.renderItem(
+ { type: 'inspectable-object', payload: { obj: { id: 'someId', type: 'message' } }},
+ 'someKey'
+ );
+ expect(inspectableObjItem).not.toBeNull();
+ expect(instance.inspectableObjects.someId).toBe(true);
+ });
+
+ it('should render a network request item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const networkReqItem = instance.renderItem(
+ {
+ type: 'network-request',
+ payload: {
+ facility: undefined,
+ body: { some: 'data' },
+ headers: undefined,
+ method: 'GET',
+ url: undefined
+ }
+ },
+ 'someKey'
+ );
+ expect(networkReqItem).not.toBeNull();
+ });
+
+ it('should render a network response item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const networkResItem = instance.renderItem(
+ {
+ type: 'network-response',
+ payload: {
+ body: { some: 'data' },
+ headers: undefined,
+ statusCode: 404,
+ statusMessage: undefined,
+ srcUrl: undefined
+ }
+ },
+ 'someKey'
+ );
+ expect(networkResItem).not.toBeNull();
+ });
+
+ it('should render an ngrok expiration item', () => {
+ wrapper = mount();
+ instance = wrapper.instance();
+ const ngrokitem = instance.renderItem(
+ { type: 'ngrok-expiration', payload: { text: 'some text' }},
+ 'someKey'
+ );
+ expect(ngrokitem).not.toBeNull();
+ });
});
diff --git a/packages/app/client/src/ui/helpers/activeBotHelper.spec.ts b/packages/app/client/src/ui/helpers/activeBotHelper.spec.ts
index a0b17e355..ca45b7004 100644
--- a/packages/app/client/src/ui/helpers/activeBotHelper.spec.ts
+++ b/packages/app/client/src/ui/helpers/activeBotHelper.spec.ts
@@ -295,6 +295,11 @@ describe('ActiveBotHelper tests', () => {
SharedConstants.Commands.Bot.SetActive,
bot
);
+ expect(mockRemoteCall).toHaveBeenCalledWith(
+ SharedConstants.Commands.Telemetry.TrackEvent,
+ 'bot_open',
+ { method: 'file_browse', numOfServices: 0 }
+ );
ActiveBotHelper.browseForBotFile = backupBrowseForBotFile;
ActiveBotHelper.botAlreadyOpen = backupBotAlreadyOpen;
diff --git a/packages/app/client/src/ui/shell/mdi/tabBar/tabBar.spec.tsx b/packages/app/client/src/ui/shell/mdi/tabBar/tabBar.spec.tsx
index 699e18538..da6760d93 100644
--- a/packages/app/client/src/ui/shell/mdi/tabBar/tabBar.spec.tsx
+++ b/packages/app/client/src/ui/shell/mdi/tabBar/tabBar.spec.tsx
@@ -48,6 +48,7 @@ import {
CONTENT_TYPE_TRANSCRIPT,
CONTENT_TYPE_WELCOME_PAGE,
} from '../../../../constants';
+import { SharedConstants } from '@bfemulator/app-shared';
import { TabBarContainer } from './tabBarContainer';
import { TabBar } from './tabBar';
@@ -74,6 +75,16 @@ jest.mock('../tab/tab', () => ({
},
}));
+let mockRemoteCallsMade;
+jest.mock('../../../../platform/commands/commandServiceImpl', () => ({
+ CommandServiceImpl: {
+ remoteCall: (commandName, ...args) => {
+ mockRemoteCallsMade.push({ commandName, args });
+ return Promise.resolve(true);
+ }
+ }
+}));
+
describe('TabBar', () => {
let wrapper;
let node;
@@ -112,6 +123,7 @@ describe('TabBar', () => {
};
mockStore = createStore((_state, _action) => defaultState);
mockDispatch = jest.spyOn(mockStore, 'dispatch');
+ mockRemoteCallsMade = [];
wrapper = mount(
@@ -125,6 +137,9 @@ describe('TabBar', () => {
instance.onPresentationModeClick();
expect(mockDispatch).toHaveBeenCalledWith(enable());
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
+ expect(mockRemoteCallsMade[0].args).toEqual(['tabBar_presentationMode']);
});
it('should load widgets', () => {
@@ -204,10 +219,13 @@ describe('TabBar', () => {
it('should handle a split click', () => {
instance.onSplitClick();
-
+
expect(mockDispatch).toHaveBeenCalledWith(
splitTab('transcript', 'doc1', 'primary', 'secondary')
);
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
+ expect(mockRemoteCallsMade[0].args).toEqual(['tabBar_splitTab']);
});
it('should handle a drag enter event', () => {
diff --git a/packages/app/client/src/ui/shell/navBar/navBar.spec.tsx b/packages/app/client/src/ui/shell/navBar/navBar.spec.tsx
new file mode 100644
index 000000000..1465feee4
--- /dev/null
+++ b/packages/app/client/src/ui/shell/navBar/navBar.spec.tsx
@@ -0,0 +1,154 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Bot Framework: http://botframework.com
+//
+// Bot Framework Emulator Github:
+// https://github.com/Microsoft/BotFramwork-Emulator
+//
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License:
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import * as React from 'react';
+import { mount, shallow } from 'enzyme';
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+import { NavBar as NavBarContainer } from './navBarContainer';
+import { NavBarComponent as NavBar } from './navBar';
+import { SharedConstants } from '@bfemulator/app-shared';
+import * as Constants from '../../../constants';
+import { select } from '../../../data/action/navBarActions';
+import { open } from '../../../data/action/editorActions';
+import { showExplorer } from '../../../data/action/explorerActions';
+
+let mockRemoteCallsMade;
+jest.mock('../../../platform/commands/commandServiceImpl', () => ({
+ CommandServiceImpl: {
+ remoteCall: jest.fn((commandName, ...args) => {
+ mockRemoteCallsMade.push({ commandName, args });
+ })
+ }
+}));
+jest.mock('./navBar.scss', () => ({}));
+
+let mockState;
+let mockNotifications = {
+ id1: { read: true },
+ id2: { read: true },
+ id3: { read: false }
+};
+jest.mock('../../../notificationManager', () => ({
+ NotificationManager: {
+ get: (id) => mockNotifications[id]
+ }
+}));
+
+describe('', () => {
+ let mockDispatch;
+ let wrapper;
+ let instance;
+ let node;
+
+ beforeEach(() => {
+ mockState = {
+ bot: {
+ activeBot: {}
+ },
+ notification: {
+ allIds: Object.keys(mockNotifications)
+ }
+ };
+ const mockStore = createStore((_state, _action) => mockState);
+ mockDispatch = jest.spyOn(mockStore, 'dispatch');
+ mockRemoteCallsMade = [];
+ wrapper = mount(
+
+
+
+ );
+ node = wrapper.find(NavBar);
+ instance = node.instance();
+ });
+
+ it('should render links for each section', () => {
+ expect(instance).not.toBeNull();
+ expect(instance.links).toHaveLength(4);
+ });
+
+ it('should render a notification badge', () => {
+ const badge = shallow(instance.renderNotificationBadge('Notifications'));
+ expect(badge.html()).not.toBeNull();
+ expect(badge.html().includes('1')).toBe(true);
+ });
+
+ it('should select the corresponding nav section', () => {
+ const parentElement: any = {
+ children: ['botExplorer', 'resources', 'settings']
+ };
+ const currentTarget = {
+ name: 'notifications',
+ parentElement
+ };
+ // wedge notifications "anchor" in between "resources" and "settings"
+ parentElement.children.splice(2, 0, currentTarget);
+ const mockEvent = {
+ currentTarget
+ };
+ instance.onLinkClick(mockEvent);
+
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
+ expect(mockRemoteCallsMade[0].args).toEqual(['navbar_selection', { selection: 'notifications' }]);
+ expect(mockDispatch).toHaveBeenCalledWith(select('navbar.notifications'));
+ expect(instance.state.selection).toBe('navbar.notifications');
+ });
+
+ it('should open the app settings editor', () => {
+ const parentElement: any = {
+ children: ['botExplorer', 'resources', 'notifications']
+ };
+ const currentTarget = {
+ name: 'settings',
+ parentElement
+ };
+ const mockEvent = {
+ currentTarget
+ };
+ instance.onLinkClick(mockEvent);
+
+ expect(mockDispatch).toHaveBeenCalledWith(open({
+ contentType: Constants.CONTENT_TYPE_APP_SETTINGS,
+ documentId: Constants.DOCUMENT_ID_APP_SETTINGS,
+ isGlobal: true,
+ meta: null
+ }));
+ });
+
+ it('should show / hide the explorer', () => {
+ instance.props.showExplorer(true);
+
+ expect(mockDispatch).toHaveBeenCalledWith(showExplorer(true));
+ });
+});
diff --git a/packages/app/main/src/appUpdater.spec.ts b/packages/app/main/src/appUpdater.spec.ts
index b33b727d0..77735308b 100644
--- a/packages/app/main/src/appUpdater.spec.ts
+++ b/packages/app/main/src/appUpdater.spec.ts
@@ -32,6 +32,7 @@
//
import { AppUpdater } from './appUpdater';
+import { TelemetryService } from './telemetry';
let mockAutoUpdater: any = {
quitAndInstall: null,
@@ -59,9 +60,18 @@ jest.mock('./settingsData/store', () => ({
}));
describe('AppUpdater', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
beforeEach(() => {
mockAutoUpdater = {};
mockSettings = { ...defaultSettings };
+ mockTrackEvent = jest.fn(() => Promise.resolve());
+ TelemetryService.trackEvent = mockTrackEvent;
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
});
it('should get userInitiated', () => {
@@ -171,15 +181,19 @@ describe('AppUpdater', () => {
AppUpdater.checkForUpdates = tmp;
});
- it('should check for updates from the stable release repo', () => {
+ it('should check for updates from the stable release repo', async () => {
const mockSetFeedURL = jest.fn((_options: any) => null);
+<<<<<<< HEAD
const mockCheckForUpdates = jest.fn((_userInitiated: boolean) =>
Promise.resolve()
);
+=======
+ const mockCheckForUpdates = jest.fn(() => Promise.resolve());
+>>>>>>> 9112640c... Added tests for Telemetry.
mockAutoUpdater.setFeedURL = mockSetFeedURL;
mockAutoUpdater.checkForUpdates = mockCheckForUpdates;
- AppUpdater.checkForUpdates(true);
+ await AppUpdater.checkForUpdates(true);
expect(AppUpdater.userInitiated).toBe(true);
@@ -190,18 +204,26 @@ describe('AppUpdater', () => {
});
expect(mockCheckForUpdates).toHaveBeenCalledTimes(1);
+ expect(mockTrackEvent).toHaveBeenCalledWith(
+ 'update_check',
+ { auto: !AppUpdater.userInitiated, prerelease: false }
+ );
});
- it('should check for updates from the nightly release repo', () => {
+ it('should check for updates from the nightly release repo', async () => {
mockSettings.usePrereleases = true;
const mockSetFeedURL = jest.fn((_options: any) => null);
+<<<<<<< HEAD
const mockCheckForUpdates = jest.fn((_userInitiated: boolean) =>
Promise.resolve()
);
+=======
+ const mockCheckForUpdates = jest.fn(() => Promise.resolve());
+>>>>>>> 9112640c... Added tests for Telemetry.
mockAutoUpdater.setFeedURL = mockSetFeedURL;
mockAutoUpdater.checkForUpdates = mockCheckForUpdates;
- AppUpdater.checkForUpdates(false);
+ await AppUpdater.checkForUpdates(false);
expect(mockSetFeedURL).toHaveBeenCalledWith({
repo: 'BotFramework-Emulator-Nightlies',
@@ -210,12 +232,20 @@ describe('AppUpdater', () => {
});
expect(mockCheckForUpdates).toHaveBeenCalledTimes(1);
+ expect(mockTrackEvent).toHaveBeenCalledWith(
+ 'update_check',
+ { auto: !AppUpdater.userInitiated, prerelease: true }
+ );
});
it('should throw if there is an error while trying to check for updates', async () => {
+<<<<<<< HEAD
const mockCheckForUpdates = jest.fn((_userInitiated: boolean) =>
Promise.reject('ERROR')
);
+=======
+ const mockCheckForUpdates = jest.fn(() => Promise.reject('ERROR'));
+>>>>>>> 9112640c... Added tests for Telemetry.
mockAutoUpdater.checkForUpdates = mockCheckForUpdates;
mockAutoUpdater.setFeedURL = () => null;
diff --git a/packages/app/main/src/commands/botCommands.spec.ts b/packages/app/main/src/commands/botCommands.spec.ts
index 2522947fd..575407280 100644
--- a/packages/app/main/src/commands/botCommands.spec.ts
+++ b/packages/app/main/src/commands/botCommands.spec.ts
@@ -57,6 +57,7 @@ import {
} from '../watchers';
import { registerCommands } from './botCommands';
+import { TelemetryService } from '../telemetry';
const mockBotConfig = BotConfiguration;
let mockStore;
@@ -133,6 +134,18 @@ jest.mock('chokidar', () => ({
const { Bot } = SharedConstants.Commands;
describe('The botCommands', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
+ beforeEach(() => {
+ mockTrackEvent = jest.fn(() => Promise.resolve());
+ TelemetryService.trackEvent = mockTrackEvent;
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
+ });
+
it('should create/save a new bot', async () => {
const botToSave = BotConfigWithPathImpl.fromJSON(mockBot as any);
const patchBotInfoSpy = jest.spyOn(
@@ -153,6 +166,7 @@ describe('The botCommands', () => {
expect(patchBotInfoSpy).toHaveBeenCalledWith(botToSave.path, mockBotInfo);
expect(saveBotSpy).toHaveBeenCalledWith(botToSave);
expect(result).toEqual(botToSave);
+ expect(mockTrackEvent).toHaveBeenCalledWith('bot_create', { path: mockBotInfo.path, hasSecret: true });
});
it('should open a bot and set the default transcript and chat path if none exists', async () => {
diff --git a/packages/app/main/src/commands/electronCommands.spec.ts b/packages/app/main/src/commands/electronCommands.spec.ts
index 6fd1a5dec..2303bdb80 100644
--- a/packages/app/main/src/commands/electronCommands.spec.ts
+++ b/packages/app/main/src/commands/electronCommands.spec.ts
@@ -41,6 +41,7 @@ import { getStore } from '../botData/store';
import { mainWindow } from '../main';
import { registerCommands } from './electronCommands';
+import { TelemetryService } from '../telemetry';
let renameArgs;
jest.mock('fs-extra', () => ({
@@ -50,6 +51,7 @@ jest.mock('fs-extra', () => ({
rename: async (...args: any[]) => (renameArgs = args),
}));
+let mockOpenExternal;
jest.mock('electron', () => ({
app: {
getName: () => 'BotFramework Emulator',
@@ -63,8 +65,11 @@ jest.mock('electron', () => ({
dialog: {
showMessageBox: () => void 0,
showOpenDialog: () => void 0,
- showSaveDialog: () => void 0,
+ showSaveDialog: () => void 0
},
+ shell: {
+ get openExternal() { return mockOpenExternal; }
+ }
}));
jest.mock('../main', () => ({
@@ -140,6 +145,18 @@ const mockCommandRegistry = new CommandRegistryImpl();
registerCommands(mockCommandRegistry);
describe('the electron commands', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
+ beforeEach(() => {
+ mockTrackEvent = jest.fn(() => Promise.resolve());
+ TelemetryService.trackEvent = mockTrackEvent;
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
+ });
+
it('should show a message box', async () => {
const { handler } = mockCommandRegistry.getCommand(
SharedConstants.Commands.Electron.ShowMessageBox
@@ -294,4 +311,14 @@ describe('the electron commands', () => {
}
expect(threw).toBeTruthy();
});
+
+ it('should open an external link', async () => {
+ mockOpenExternal = jest.fn(() => null);
+ const { handler } = mockCommandRegistry.getCommand(SharedConstants.Commands.Electron.OpenExternal);
+ const url = 'https://aka.ms/bf-emulator-testing';
+ await handler(url);
+
+ expect(mockTrackEvent).toHaveBeenCalledWith('app_openLink', { url });
+ expect(mockOpenExternal).toHaveBeenCalledWith(url, { activate: true });
+ });
});
diff --git a/packages/app/main/src/commands/emulatorCommands.spec.ts b/packages/app/main/src/commands/emulatorCommands.spec.ts
index 3ba6be6c3..b86182376 100644
--- a/packages/app/main/src/commands/emulatorCommands.spec.ts
+++ b/packages/app/main/src/commands/emulatorCommands.spec.ts
@@ -34,7 +34,6 @@
import '../fetchProxy';
import * as path from 'path';
-
import { combineReducers, createStore } from 'redux';
import {
BotConfigWithPathImpl,
@@ -43,15 +42,14 @@ import {
import { BotConfiguration } from 'botframework-config';
import { newBot, newEndpoint, SharedConstants } from '@bfemulator/app-shared';
import { Conversation } from '@bfemulator/emulator-core';
-
import * as store from '../botData/store';
import { emulator } from '../emulator';
import * as utils from '../utils';
import * as botHelpers from '../botHelpers';
import { bot } from '../botData/reducers/bot';
import * as BotActions from '../botData/actions/botActions';
+import { TelemetryService } from '../telemetry';
import { mainWindow } from '../main';
-
import { registerCommands } from './emulatorCommands';
const mockBotConfig = BotConfiguration;
@@ -391,8 +389,17 @@ registerCommands(mockCommandRegistry);
const { Emulator } = SharedConstants.Commands;
describe('The emulatorCommands', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
beforeEach(() => {
mockUsers = { users: {} };
+ mockTrackEvent = jest.fn(() => Promise.resolve());
+ TelemetryService.trackEvent = mockTrackEvent;
+ });
+
+ beforeAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
});
it('should save a transcript to file based on the transcripts path in the botInfo', async () => {
@@ -447,10 +454,15 @@ describe('The emulatorCommands', () => {
const newPath = path.normalize('chosen/AuthBot.bot');
expect(getBotInfoByPathSpy).toHaveBeenCalledWith('some/path');
expect(toSavableBotSpy).toHaveBeenCalledWith(mockBot, mockInfo.secret);
+<<<<<<< HEAD
expect(patchBotJsonSpy).toHaveBeenCalledWith(
newPath,
Object.assign({}, mockInfo, { path: newPath })
);
+=======
+ expect(patchBotJsonSpy).toHaveBeenCalledWith(newPath, Object.assign({}, mockInfo, { path: newPath }));
+ expect(mockTrackEvent).toHaveBeenCalledWith('transcript_save');
+>>>>>>> 9112640c... Added tests for Telemetry.
});
it('should feed a transcript from disk to a conversation', async () => {
diff --git a/packages/app/main/src/commands/settingsCommands.spec.ts b/packages/app/main/src/commands/settingsCommands.spec.ts
new file mode 100644
index 000000000..c87330c73
--- /dev/null
+++ b/packages/app/main/src/commands/settingsCommands.spec.ts
@@ -0,0 +1,80 @@
+import { CommandRegistryImpl } from "@bfemulator/sdk-shared";
+
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Bot Framework: http://botframework.com
+//
+// Bot Framework Emulator Github:
+// https://github.com/Microsoft/BotFramwork-Emulator
+//
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License:
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import { registerCommands } from './settingsCommands';
+import { TelemetryService } from "../telemetry";
+import { SharedConstants } from "@bfemulator/app-shared";
+import { setFramework } from '../settingsData/actions/frameworkActions';
+
+const mockSettings = { framework: { ngrokPath: 'path/to/ngrok.exe' }}
+let mockDispatch;
+jest.mock('../settingsData/store', () => ({
+ get dispatch() { return mockDispatch; },
+ getSettings: () => mockSettings
+}));
+
+const mockRegistry = new CommandRegistryImpl();
+registerCommands(mockRegistry);
+
+describe('The settings commands', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
+ beforeEach(() => {
+ mockTrackEvent = jest.fn(() => Promise.resolve());
+ TelemetryService.trackEvent = mockTrackEvent;
+ mockDispatch = jest.fn(() => null);
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
+ });
+
+ it('should save the global app settings', async () => {
+ const { handler } = mockRegistry.getCommand(SharedConstants.Commands.Settings.SaveAppSettings);
+ const mockSettings = { ngrokPath: 'other/path/to/ngrok.exe' };
+ await handler(mockSettings);
+
+ expect(mockTrackEvent).toHaveBeenCalledWith('app_configureNgrok');
+ expect(mockDispatch).toHaveBeenCalledWith(setFramework(mockSettings));
+ });
+
+ it('should load the app settings from the store', async () => {
+ const { handler } = mockRegistry.getCommand(SharedConstants.Commands.Settings.LoadAppSettings);
+ const appSettings = await handler();
+
+ expect(appSettings).toBe(mockSettings.framework);
+ });
+});
diff --git a/packages/app/main/src/commands/telemetryCommands.spec.ts b/packages/app/main/src/commands/telemetryCommands.spec.ts
new file mode 100644
index 000000000..d728b55b8
--- /dev/null
+++ b/packages/app/main/src/commands/telemetryCommands.spec.ts
@@ -0,0 +1,68 @@
+import { CommandRegistryImpl } from "@bfemulator/sdk-shared";
+
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Bot Framework: http://botframework.com
+//
+// Bot Framework Emulator Github:
+// https://github.com/Microsoft/BotFramwork-Emulator
+//
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License:
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import { registerCommands } from './telemetryCommands';
+import { TelemetryService } from "../telemetry";
+import { SharedConstants } from "@bfemulator/app-shared";
+
+jest.mock('../settingsData/store', () => ({
+ getSettings: () => ({
+ framework: {}
+ })
+}));
+
+const mockRegistry = new CommandRegistryImpl();
+registerCommands(mockRegistry);
+
+describe('The telemetry commands', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
+ beforeEach(() => {
+ mockTrackEvent = jest.fn(() => Promise.resolve());
+ TelemetryService.trackEvent = mockTrackEvent;
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
+ });
+
+ it('should track events to App Insights', async () => {
+ const { handler } = mockRegistry.getCommand(SharedConstants.Commands.Telemetry.TrackEvent);
+ await handler('test_event', { some: 'data' });
+
+ expect(mockTrackEvent).toHaveBeenCalledWith('test_event', { some: 'data' });
+ });
+});
diff --git a/packages/app/main/src/protocolHandler.spec.ts b/packages/app/main/src/protocolHandler.spec.ts
index f4818d6d0..2e6184d50 100644
--- a/packages/app/main/src/protocolHandler.spec.ts
+++ b/packages/app/main/src/protocolHandler.spec.ts
@@ -45,13 +45,99 @@ import {
ProtocolHandler,
parseEndpointOverrides,
} from './protocolHandler';
-jest.mock('./main', () => ({}));
+import { TelemetryService } from './telemetry';
+import { SharedConstants, newBot, newEndpoint } from '@bfemulator/app-shared';
+import { applyBotConfigOverrides, BotConfigWithPathImpl } from '@bfemulator/sdk-shared';
+
+let mockCallsMade, mockRemoteCallsMade;
+let mockOpenedBot;
+let mockSharedConstants = SharedConstants;
+jest.mock('./main', () => ({
+ mainWindow: {
+ commandService: {
+ call: (commandName, ...args) => {
+ mockCallsMade.push({ commandName, args });
+ if (commandName === mockSharedConstants.Commands.Bot.Open) {
+ return Promise.resolve(mockOpenedBot);
+ }
+ },
+ remoteCall: (commandName, ...args) => {
+ mockRemoteCallsMade.push({ commandName, args })
+ }
+ }
+ }
+}));
jest.mock('./globals', () => ({
getGlobal: () => ({}),
- setGlobal: () => null,
+ setGlobal: () => null
+}));
+
+let mockNgrokPath;
+jest.mock('./settingsData/store', () => ({
+ getSettings: () => ({
+ framework: {
+ ngrokPath: mockNgrokPath
+ }
+ })
+}));
+jest.mock('./emulator', () => ({
+ emulator: {
+ ngrok: {
+ getSpawnStatus: () => ({ triedToSpawn: true })
+ }
+ }
+}));
+
+let mockRunningStatus;
+jest.mock('./ngrok', () => ({
+ ngrokEmitter: {
+ once: (_eventName, cb) => cb()
+ },
+ running: () => mockRunningStatus
}));
+let mockSendNotificationToClient;
+jest.mock('./utils/sendNotificationToClient', () => ({
+ sendNotificationToClient: () => mockSendNotificationToClient()
+}));
+
+let mockGotReturnValue;
+jest.mock('got', () => {
+ return jest.fn(() => Promise.resolve(mockGotReturnValue))
+});
+
describe('Protocol handler tests', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
+ beforeEach(() => {
+ mockTrackEvent = jest.fn(() => null);
+ TelemetryService.trackEvent = mockTrackEvent;
+ mockCallsMade = [];
+ mockRemoteCallsMade = [];
+ mockOpenedBot = {
+ name: 'someBot',
+ description: '',
+ path: 'path/to/bot.bot',
+ services: [{
+ appId: 'someAppId',
+ appPassword: 'somePw',
+ endpoint: 'https://www.myendpoint.com'
+ }]
+ };
+ mockRunningStatus = true;
+ mockNgrokPath = 'path/to/ngrok.exe';
+ mockSendNotificationToClient = jest.fn(() => null);
+ mockGotReturnValue = {
+ statusCode: 200,
+ body: '["activity1", "activity2", "activity3"]'
+ };
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
+ });
+
describe('parseProtocolUrl() functionality', () => {
it('should return an info object about the parsed URL', () => {
const info: Protocol = ProtocolHandler.parseProtocolUrl(
@@ -108,7 +194,7 @@ describe('Protocol handler tests', () => {
ProtocolHandler.performTranscriptAction = tmpPerformTranscriptAction;
});
- it('shouldn\t do anything on an unrecognized action', () => {
+ it('shouldn\'t do anything on an unrecognized action', () => {
const mockPerformBotAction = jest.fn(() => null);
ProtocolHandler.performBotAction = mockPerformBotAction;
const mockPerformLiveChatAction = jest.fn(() => null);
@@ -188,7 +274,202 @@ describe('Protocol handler tests', () => {
});
});
- // unmock mainWindow
- jest.unmock('./main');
- jest.unmock('./globals');
+ it('should open a bot when ngrok is running', async () => {
+ const protocol = {
+ parsedArgs: {
+ id: 'someIdOverride',
+ path: 'path/to/bot.bot',
+ secret: 'someSecret'
+ }
+ };
+ const overrides = { endpoint: parseEndpointOverrides(protocol.parsedArgs) };
+ const overriddenBot = applyBotConfigOverrides(mockOpenedBot, overrides);
+
+ await ProtocolHandler.openBot(protocol);
+
+ expect(mockCallsMade).toHaveLength(2);
+ expect(mockCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Open);
+ expect(mockCallsMade[0].args).toEqual(['path/to/bot.bot', 'someSecret']);
+ expect(mockCallsMade[1].commandName).toBe(SharedConstants.Commands.Bot.SetActive);
+ expect(mockCallsMade[1].args).toEqual([overriddenBot]);
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Load);
+ expect(mockRemoteCallsMade[0].args).toEqual([overriddenBot]);
+ expect(mockTrackEvent).toHaveBeenCalledWith('bot_open', { method: 'protocol', numOfServices: 1 });
+ });
+
+ it('should open a bot when ngrok is configured but not running', async () => {
+ mockRunningStatus = false
+ const protocol = {
+ parsedArgs: {
+ id: 'someIdOverride',
+ path: 'path/to/bot.bot',
+ secret: 'someSecret'
+ }
+ };
+ const overrides = { endpoint: parseEndpointOverrides(protocol.parsedArgs) };
+ const overriddenBot = applyBotConfigOverrides(mockOpenedBot, overrides);
+
+ await ProtocolHandler.openBot(protocol);
+
+ expect(mockCallsMade).toHaveLength(2);
+ expect(mockCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Open);
+ expect(mockCallsMade[0].args).toEqual(['path/to/bot.bot', 'someSecret']);
+ expect(mockCallsMade[1].commandName).toBe(SharedConstants.Commands.Bot.SetActive);
+ expect(mockCallsMade[1].args).toEqual([overriddenBot]);
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Load);
+ expect(mockRemoteCallsMade[0].args).toEqual([overriddenBot]);
+ expect(mockTrackEvent).toHaveBeenCalledWith('bot_open', { method: 'protocol', numOfServices: 1 });
+ });
+
+ it('should open a bot when ngrok is not configured', async () => {
+ mockNgrokPath = undefined;
+ const protocol = {
+ parsedArgs: {
+ id: 'someIdOverride',
+ path: 'path/to/bot.bot',
+ secret: 'someSecret'
+ }
+ };
+ const overrides = { endpoint: parseEndpointOverrides(protocol.parsedArgs) };
+ const overriddenBot = applyBotConfigOverrides(mockOpenedBot, overrides);
+
+ await ProtocolHandler.openBot(protocol);
+
+ expect(mockCallsMade).toHaveLength(2);
+ expect(mockCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Open);
+ expect(mockCallsMade[0].args).toEqual(['path/to/bot.bot', 'someSecret']);
+ expect(mockCallsMade[1].commandName).toBe(SharedConstants.Commands.Bot.SetActive);
+ expect(mockCallsMade[1].args).toEqual([overriddenBot]);
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.Load);
+ expect(mockRemoteCallsMade[0].args).toEqual([overriddenBot]);
+ expect(mockTrackEvent).toHaveBeenCalledWith('bot_open', { method: 'protocol', numOfServices: 1 });
+ });
+
+ it('should open a livechat if ngrok is running', async () => {
+ const protocol = {
+ parsedArgs: {
+ botUrl: 'someUrl',
+ msaAppId: 'someAppId',
+ msaPassword: 'somePw'
+ }
+ };
+ const mockedBot = BotConfigWithPathImpl.fromJSON(newBot());
+ mockedBot.name = '';
+ mockedBot.path = SharedConstants.TEMP_BOT_IN_MEMORY_PATH;
+
+ const mockEndpoint = newEndpoint();
+ mockEndpoint.appId = protocol.parsedArgs.msaAppId;
+ mockEndpoint.appPassword = protocol.parsedArgs.msaPassword;
+ mockEndpoint.id = mockEndpoint.endpoint = protocol.parsedArgs.botUrl
+ mockEndpoint.name = 'New livechat';
+ mockedBot.services.push(mockEndpoint);
+
+ await ProtocolHandler.openLiveChat(protocol);
+
+ expect(mockCallsMade).toHaveLength(1);
+ expect(mockCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.RestartEndpointService);
+ expect(mockCallsMade[0].args).toEqual([]);
+ expect(mockRemoteCallsMade).toHaveLength(2);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.SetActive);
+ expect(mockRemoteCallsMade[0].args).toEqual([mockedBot, '']);
+ expect(mockRemoteCallsMade[1].commandName).toBe(SharedConstants.Commands.Emulator.NewLiveChat);
+ expect(mockRemoteCallsMade[1].args).toEqual([mockEndpoint]);
+ });
+
+ it('should open a livechat if ngrok is configured but not running', async () => {
+ mockRunningStatus = false;
+ const protocol = {
+ parsedArgs: {
+ botUrl: 'someUrl',
+ msaAppId: 'someAppId',
+ msaPassword: 'somePw'
+ }
+ };
+ const mockedBot = BotConfigWithPathImpl.fromJSON(newBot());
+ mockedBot.name = '';
+ mockedBot.path = SharedConstants.TEMP_BOT_IN_MEMORY_PATH;
+
+ const mockEndpoint = newEndpoint();
+ mockEndpoint.appId = protocol.parsedArgs.msaAppId;
+ mockEndpoint.appPassword = protocol.parsedArgs.msaPassword;
+ mockEndpoint.id = mockEndpoint.endpoint = protocol.parsedArgs.botUrl
+ mockEndpoint.name = 'New livechat';
+ mockedBot.services.push(mockEndpoint);
+
+ await ProtocolHandler.openLiveChat(protocol);
+
+ expect(mockCallsMade).toHaveLength(1);
+ expect(mockCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.RestartEndpointService);
+ expect(mockCallsMade[0].args).toEqual([]);
+ expect(mockRemoteCallsMade).toHaveLength(2);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Bot.SetActive);
+ expect(mockRemoteCallsMade[0].args).toEqual([mockedBot, '']);
+ expect(mockRemoteCallsMade[1].commandName).toBe(SharedConstants.Commands.Emulator.NewLiveChat);
+ expect(mockRemoteCallsMade[1].args).toEqual([mockEndpoint]);
+ });
+
+ it('should open a livechat if ngrok is not configured', async () => {
+ mockNgrokPath = undefined;
+ const protocol = {
+ parsedArgs: {
+ botUrl: 'someUrl',
+ msaAppId: 'someAppId',
+ msaPassword: 'somePw'
+ }
+ };
+ const mockedBot = BotConfigWithPathImpl.fromJSON(newBot());
+ mockedBot.name = '';
+ mockedBot.path = SharedConstants.TEMP_BOT_IN_MEMORY_PATH;
+
+ const mockEndpoint = newEndpoint();
+ mockEndpoint.appId = protocol.parsedArgs.msaAppId;
+ mockEndpoint.appPassword = protocol.parsedArgs.msaPassword;
+ mockEndpoint.id = mockEndpoint.endpoint = protocol.parsedArgs.botUrl
+ mockEndpoint.name = 'New livechat';
+ mockedBot.services.push(mockEndpoint);
+
+ await ProtocolHandler.openLiveChat(protocol);
+
+ expect(mockCallsMade).toHaveLength(0);
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.NewLiveChat);
+ expect(mockRemoteCallsMade[0].args).toEqual([mockEndpoint]);
+ });
+
+ it('should open a transcript from a url', async () => {
+ const protocol = {
+ parsedArgs: { url: 'https://www.test.com/convo1.transcript' }
+ };
+
+ await ProtocolHandler.openTranscript(protocol);
+
+ expect(mockRemoteCallsMade).toHaveLength(1);
+ expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.OpenTranscript);
+ expect(mockRemoteCallsMade[0].args).toEqual([
+ 'deepLinkedTranscript',
+ { activities: ['activity1', 'activity2', 'activity3' ], inMemory: true, fileName: 'convo1.transcript' }]
+ );
+ });
+
+ it('should send a notification if trying to open a transcript from a url results in a 401 or 404', async () => {
+ const protocol = {
+ parsedArgs: { url: 'https://www.test.com/convo1.transcript' }
+ };
+ mockGotReturnValue = { statusCode: 401 };
+
+ await ProtocolHandler.openTranscript(protocol);
+
+ expect(mockRemoteCallsMade).toHaveLength(0);
+ expect(mockSendNotificationToClient).toHaveBeenCalledTimes(1);
+
+ mockGotReturnValue = { statusCode: 404 };
+
+ await ProtocolHandler.openTranscript(protocol);
+
+ expect(mockRemoteCallsMade).toHaveLength(0);
+ expect(mockSendNotificationToClient).toHaveBeenCalledTimes(2);
+ });
});
diff --git a/packages/app/main/src/protocolHandler.ts b/packages/app/main/src/protocolHandler.ts
index a6cc1a22b..286b4d18b 100644
--- a/packages/app/main/src/protocolHandler.ts
+++ b/packages/app/main/src/protocolHandler.ts
@@ -167,7 +167,6 @@ export const ProtocolHandler = new class ProtocolHandlerImpl
switch (ProtocolTranscriptActions[protocol.action]) {
case ProtocolTranscriptActions.open:
this.openTranscript(protocol);
- TelemetryService.trackEvent('transcriptFile_open', { method: 'protocol' });
break;
default:
@@ -263,7 +262,7 @@ export const ProtocolHandler = new class ProtocolHandlerImpl
const { url } = protocol.parsedArgs;
const options = { url };
- got(options)
+ return got(options)
.then(res => {
if (/^2\d\d$/.test(res.statusCode)) {
if (res.body) {
@@ -334,14 +333,10 @@ export const ProtocolHandler = new class ProtocolHandlerImpl
secret
);
if (!bot) {
- throw new Error(
- `Error occurred while trying to open bot at: ${path} inside of protocol handler.`
- );
+ throw new Error(`Error occurred while trying to open bot at ${path} inside of protocol handler: Bot is invalid.`);
}
} catch (e) {
- throw new Error(
- `Error occurred while trying to open bot at: ${path} inside of protocol handler.`
- );
+ throw new Error(`Error occurred while trying to open bot at ${path} inside of protocol handler: ${e}`);
}
// apply any overrides
@@ -374,7 +369,7 @@ export const ProtocolHandler = new class ProtocolHandlerImpl
);
} catch (e) {
throw new Error(
- `(ngrok running) Error occurred while trying to deep link to bot project at: ${path}.`
+ `(ngrok running) Error occurred while trying to deep link to bot project at ${path}: ${e}`
);
}
} else {
@@ -393,7 +388,8 @@ export const ProtocolHandler = new class ProtocolHandlerImpl
);
} catch (e) {
throw new Error(
- `(ngrok running but not connected) Error occurred while trying to deep link to bot project at: ${path}.`
+ `(ngrok running but not connected) Error occurred while ` +
+ `trying to deep link to bot project at ${path}: ${e}`
);
}
}
@@ -411,7 +407,7 @@ export const ProtocolHandler = new class ProtocolHandlerImpl
);
} catch (e) {
throw new Error(
- `(ngrok not configured) Error occurred while trying to deep link to bot project at: ${path}`
+ `(ngrok not configured) Error occurred while trying to deep link to bot project at ${path}: ${e}`
);
}
}
diff --git a/packages/app/main/src/services/conversationService.spec.ts b/packages/app/main/src/services/conversationService.spec.ts
index 956599d18..f68825877 100644
--- a/packages/app/main/src/services/conversationService.spec.ts
+++ b/packages/app/main/src/services/conversationService.spec.ts
@@ -30,14 +30,15 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
-import '../fetchProxy';
+import { TelemetryService } from '../telemetry';
+import '../fetchProxy';
import {
ConversationService,
headers as headersInstance,
} from './conversationService';
-let mockFetchArgs: MockFetch;
+let mockFetchArgs: MockFetch;
jest.mock('node-fetch', () => {
const fetch = (url, opts) => {
mockFetchArgs = { url, opts };
@@ -51,6 +52,9 @@ jest.mock('node-fetch', () => {
(fetch as any).Response = class {};
return fetch;
});
+jest.mock('../settingsData/store', () => ({
+ getSettings: () => null
+}));
interface MockOpts {
headers: Headers;
@@ -64,6 +68,18 @@ interface MockFetch {
}
describe('The ConversationService should call "fetch" with the expected parameters when executing', () => {
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
+ beforeEach(() => {
+ mockTrackEvent = jest.fn(() => null);
+ TelemetryService.trackEvent = mockTrackEvent;
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
+ });
+
test('the "addUser" function', () => {
ConversationService.addUser('http://localhost', 'abcdef');
const { url, opts } = mockFetchArgs;
@@ -76,6 +92,7 @@ describe('The ConversationService should call "fetch" with the expected paramete
expect(members[0].name).toBeFalsy();
expect(members[0].id).toBeFalsy();
expect(headersInstance).toEqual(headers);
+ expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_addUser');
});
test('the "removeUser" function', () => {
@@ -89,6 +106,7 @@ describe('The ConversationService should call "fetch" with the expected paramete
const users = JSON.parse(body);
expect(users[0].id).toBe('1234');
expect(headersInstance).toEqual(headers);
+ expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_removeUser');
});
test('the "removeRandomUser" function', () => {
@@ -113,6 +131,7 @@ describe('The ConversationService should call "fetch" with the expected paramete
expect(method).toBe('POST');
expect(body).toBeFalsy();
expect(headersInstance).toEqual(headers);
+ expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_botContactAdded');
});
test('the "botContactRemoved" function', () => {
@@ -125,6 +144,7 @@ describe('The ConversationService should call "fetch" with the expected paramete
expect(method).toBe('DELETE');
expect(body).toBeFalsy();
expect(headersInstance).toEqual(headers);
+ expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_botContactRemoved');
});
test('the "typing" function', () => {
@@ -137,6 +157,7 @@ describe('The ConversationService should call "fetch" with the expected paramete
expect(method).toBe('POST');
expect(body).toBeFalsy();
expect(headersInstance).toEqual(headers);
+ expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_typing');
});
test('the "ping" function', () => {
@@ -149,6 +170,7 @@ describe('The ConversationService should call "fetch" with the expected paramete
expect(method).toBe('POST');
expect(body).toBeFalsy();
expect(headersInstance).toEqual(headers);
+ expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_ping');
});
test('the "deleteUserData" function', () => {
@@ -161,5 +183,6 @@ describe('The ConversationService should call "fetch" with the expected paramete
expect(method).toBe('DELETE');
expect(body).toBeFalsy();
expect(headersInstance).toEqual(headers);
+ expect(mockTrackEvent).toHaveBeenCalledWith('sendActivity_deleteUserData');
});
});
diff --git a/packages/app/main/src/telemetry/telemetryService.spec.ts b/packages/app/main/src/telemetry/telemetryService.spec.ts
new file mode 100644
index 000000000..9d8d054ab
--- /dev/null
+++ b/packages/app/main/src/telemetry/telemetryService.spec.ts
@@ -0,0 +1,138 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+//
+// Microsoft Bot Framework: http://botframework.com
+//
+// Bot Framework Emulator Github:
+// https://github.com/Microsoft/BotFramwork-Emulator
+//
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License:
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import { TelemetryService } from './telemetryService';
+
+let mockAppInsights;
+let mockSetup;
+let mockDefaultClient;
+let mockStart;
+jest.mock('applicationinsights', () => ({
+ get defaultClient() { return mockDefaultClient; },
+ get setup() { return mockSetup; },
+ get start() { return mockStart; }
+}));
+
+let mockSettings;
+jest.mock('../settingsData/store', () => ({
+ getSettings: () => mockSettings
+}));
+
+describe('TelemetryService', () => {
+ let tmpClient;
+ let tmpStartup;
+
+ beforeEach(() => {
+ mockDefaultClient = {
+ context: {
+ keys: {
+ cloudRoleInstance: 'cloudRoleInstance'
+ },
+ tags: {
+ cloudRoleInstance: 'SOME-MACHINE-NAME'
+ }
+ }
+ };
+ mockAppInsights = {};
+ mockSettings = { framework: { collectUsageData: true } };
+ mockSetup = jest.fn((_iKey: string) => mockAppInsights);
+ mockStart = jest.fn(() => null);
+ tmpClient = (TelemetryService as any)._client;
+ tmpStartup = (TelemetryService as any).startup;
+ });
+
+ afterEach(() => {
+ (TelemetryService as any)._client = tmpClient;
+ (TelemetryService as any).startup = tmpStartup;
+ });
+
+ it('should startup', () => {
+ const mockAutoCollect = jest.fn(_config => mockAppInsights);
+ mockAppInsights = {
+ setAutoCollectConsole: mockAutoCollect,
+ setAutoCollectDependencies: mockAutoCollect,
+ setAutoCollectExceptions: mockAutoCollect,
+ setAutoCollectPerformance: mockAutoCollect,
+ setAutoCollectRequests: mockAutoCollect
+ };
+ (TelemetryService as any).startup();
+
+ expect(mockSetup).toHaveBeenCalledTimes(1);
+ expect(mockSetup).toHaveBeenCalledWith('631faf57-1d84-40b4-9a71-fce28a3934a8');
+ expect(mockAutoCollect).toHaveBeenCalledTimes(5);
+ expect(mockStart).toHaveBeenCalledTimes(1);
+ expect(mockDefaultClient.context.tags.cloudRoleInstance).toBe('');
+ expect((TelemetryService as any)._hasStarted).toBe(true);
+ expect((TelemetryService as any)._client).toBe(mockDefaultClient);
+ });
+
+ it('should toggle enabled / disabled state based on app settings', () => {
+ mockSettings = { framework: { collectUsageData: false } };
+ expect((TelemetryService as any).enabled).toBe(false);
+
+ mockSettings = { framework: { collectUsageData: true } };
+ expect((TelemetryService as any).enabled).toBe(true);
+ });
+
+ it('should not track events if disabled or if no name is provided', () => {
+ const mockAITrackEvent = jest.fn((_name, _properties) => null);
+ (TelemetryService as any)._client = { trackEvent: mockAITrackEvent };
+
+ mockSettings = { framework: { collectUsageData: false } };
+ TelemetryService.trackEvent(null, null);
+ expect(mockAITrackEvent).not.toHaveBeenCalled();
+
+ mockSettings = { framework: { collectUsageData: true } };
+ TelemetryService.trackEvent('', {});
+ expect(mockAITrackEvent).not.toHaveBeenCalled();
+ });
+
+ it('should track events', () => {
+ const mockStartup = jest.fn(() => null);
+ (TelemetryService as any).startup = mockStartup;
+ const mockAutoCollect = jest.fn(_config => mockAppInsights);
+ mockAppInsights = {
+ setAutoCollectConsole: mockAutoCollect,
+ setAutoCollectDependencies: mockAutoCollect,
+ setAutoCollectExceptions: mockAutoCollect,
+ setAutoCollectPerformance: mockAutoCollect,
+ setAutoCollectRequests: mockAutoCollect
+ };
+ const mockAITrackEvent = jest.fn((_name, _properties) => null);
+ (TelemetryService as any)._client = { trackEvent: mockAITrackEvent };
+
+ TelemetryService.trackEvent('someEvent', { some: 'property' });
+ expect(mockStartup).toHaveBeenCalled;
+ expect(mockAITrackEvent).toHaveBeenCalledWith({ name: 'someEvent', properties: { some: 'property'} });
+ });
+});
diff --git a/packages/app/main/src/telemetry/telemetryService.ts b/packages/app/main/src/telemetry/telemetryService.ts
index dfd5a6ac6..e33adf71b 100644
--- a/packages/app/main/src/telemetry/telemetryService.ts
+++ b/packages/app/main/src/telemetry/telemetryService.ts
@@ -60,7 +60,7 @@ export class TelemetryService {
private static startup(): void {
if (!this._hasStarted) {
AppInsights
- .setup(INSTRUMENTATION_KEY)
+ .setup(INSTRUMENTATION_KEY)
// turn off extra instrmentation
.setAutoCollectConsole(false)
.setAutoCollectDependencies(false)
diff --git a/packages/app/main/src/utils/openFileFromCommandLine.spec.ts b/packages/app/main/src/utils/openFileFromCommandLine.spec.ts
index fc943b3ea..587b75d4c 100644
--- a/packages/app/main/src/utils/openFileFromCommandLine.spec.ts
+++ b/packages/app/main/src/utils/openFileFromCommandLine.spec.ts
@@ -30,6 +30,7 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
+
import {
CommandHandler,
CommandRegistry,
@@ -39,8 +40,11 @@ import {
} from '@bfemulator/sdk-shared';
import { openFileFromCommandLine } from './openFileFromCommandLine';
-import * as readFileSyncUtil from './readFileSync';
+import { TelemetryService } from '../telemetry';
+jest.mock('../settingsData/store', () => ({
+ getSettings: () => null
+}));
jest.mock('./readFileSync', () => ({
readFileSync: file => {
if (file.includes('error.transcript')) {
@@ -78,8 +82,17 @@ class MockCommandService extends DisposableImpl implements CommandService {
describe('The openFileFromCommandLine util', () => {
let commandService: MockCommandService;
+ let mockTrackEvent;
+ const trackEventBackup = TelemetryService.trackEvent;
+
beforeEach(() => {
commandService = new MockCommandService();
+ mockTrackEvent = jest.fn(() => null);
+ TelemetryService.trackEvent = mockTrackEvent;
+ });
+
+ afterAll(() => {
+ TelemetryService.trackEvent = trackEventBackup;
});
it('should make the appropriate calls to open a .bot file', async () => {
@@ -103,6 +116,7 @@ describe('The openFileFromCommandLine util', () => {
},
],
]);
+ expect(mockTrackEvent).toHaveBeenCalledWith('transcriptFile_open', { method: 'protocol' });
});
it('should throw when the transcript is not an array', async () => {
diff --git a/packages/app/main/src/utils/openFileFromCommandLine.ts b/packages/app/main/src/utils/openFileFromCommandLine.ts
index d8a480491..4a3ff99db 100644
--- a/packages/app/main/src/utils/openFileFromCommandLine.ts
+++ b/packages/app/main/src/utils/openFileFromCommandLine.ts
@@ -30,11 +30,11 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
-import * as path from 'path';
+import * as path from 'path';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandService } from '@bfemulator/sdk-shared';
-
+import { TelemetryService } from '../telemetry';
import { readFileSync } from './readFileSync';
export async function openFileFromCommandLine(
@@ -71,5 +71,6 @@ export async function openFileFromCommandLine(
inMemory: true,
}
);
+ TelemetryService.trackEvent('transcriptFile_open', { method: 'protocol' });
}
}