diff --git a/packages/botkit/package.json b/packages/botkit/package.json index 172e0e4bf..04ebe152b 100644 --- a/packages/botkit/package.json +++ b/packages/botkit/package.json @@ -10,7 +10,8 @@ ], "scripts": { "build": "tsc", - "eslint": "./node_modules/.bin/eslint --fix src/*" + "eslint": "./node_modules/.bin/eslint --fix src/*", + "test": "tsc ; nyc mocha tests/*.tests.js" }, "author": "benbrown@gmail.com", "license": "MIT", diff --git a/packages/botkit/src/index.ts b/packages/botkit/src/index.ts index 638dcf846..c99f5b900 100644 --- a/packages/botkit/src/index.ts +++ b/packages/botkit/src/index.ts @@ -10,3 +10,4 @@ export * from './core'; export * from './conversation'; export * from './botworker'; export * from './dialogWrapper'; +export * from './testClient'; diff --git a/packages/botkit/src/testClient.ts b/packages/botkit/src/testClient.ts new file mode 100644 index 000000000..7fcd54799 --- /dev/null +++ b/packages/botkit/src/testClient.ts @@ -0,0 +1,116 @@ +/** + * @module botkit + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { + Activity, + AutoSaveStateMiddleware, + ConversationState, + MemoryStorage, + Middleware, + TestAdapter, + TurnContext +} from 'botbuilder-core'; +import { Dialog, DialogSet, DialogTurnResult, DialogTurnStatus } from 'botbuilder-dialogs'; +import { Botkit } from "./core"; + +/** + * A client for testing dialogs in isolation. + */ +export class BotkitTestClient { + + private readonly _callback: (turnContext: TurnContext) => Promise; + private readonly _testAdapter: TestAdapter; + public dialogTurnResult: DialogTurnResult; + public conversationState: ConversationState; + + /** + * Create a BotkitTestClient to test a dialog without having to create a full-fledged adapter. + * + * ```javascript + * let client = new BotkitTestClient('test', bot, MY_DIALOG, MY_OPTIONS); + * let reply = await client.sendActivity('first message'); + * assert.strictEqual(reply.text, 'first reply', 'reply failed'); + * ``` + * + * @param channelId The channelId to be used for the test. + * Use 'emulator' or 'test' if you are uncertain of the channel you are targeting. + * Otherwise, it is recommended that you use the id for the channel(s) your bot will be using and write a test case for each channel. + * @param bot (Required) The Botkit bot that has the skill to test. + * @param dialogToTest (Required) The identifier of the skill to test in the bot. + * @param initialDialogOptions (Optional) additional argument(s) to pass to the dialog being started. + * @param middlewares (Optional) a stack of middleware to be run when testing + * @param conversationState (Optional) A ConversationState instance to use in the test client + */ + public constructor(channelId: string, bot: Botkit, dialogToTest: string, initialDialogOptions?: any, middlewares?: Middleware[], conversationState?: ConversationState); + public constructor(testAdapter: TestAdapter, bot: Botkit, dialogToTest: string, initialDialogOptions?: any, middlewares?: Middleware[], conversationState?: ConversationState) + constructor(channelOrAdapter: string | TestAdapter, bot: Botkit, dialogToTest: string, initialDialogOptions?: any, middlewares?: Middleware[], conversationState?: ConversationState) { + this.conversationState = conversationState || new ConversationState(new MemoryStorage()); + + let dialogState = this.conversationState.createProperty('DialogState'); + + const targetDialogs = [ + bot.dialogSet.find(dialogToTest), + bot.dialogSet.find(dialogToTest + '_default_prompt'), + bot.dialogSet.find(dialogToTest + ':botkit-wrapper'), + ]; + + this._callback = this.getDefaultCallback(targetDialogs, initialDialogOptions || null, dialogState); + + if (typeof channelOrAdapter == 'string') { + this._testAdapter = new TestAdapter(this._callback, {channelId: channelOrAdapter}).use(new AutoSaveStateMiddleware(this.conversationState)); + } else { + this._testAdapter = channelOrAdapter; + } + + this.addUserMiddlewares(middlewares); + } + + /** + * Send an activity into the dialog. + * @returns a TestFlow that can be used to assert replies etc + * @param activity an activity potentially with text + * + * ```javascript + * DialogTest.send('hello').assertReply('hello yourself').then(done); + * ``` + */ + public async sendActivity(activity: Partial | string): Promise { + await this._testAdapter.receiveActivity(activity); + return this._testAdapter.activityBuffer.shift(); + } + + /** + * Get the next reply waiting to be delivered (if one exists) + */ + public getNextReply() { + return this._testAdapter.activityBuffer.shift(); + } + + private getDefaultCallback(targetDialogs: Dialog[], initialDialogOptions: any, dialogState: any): (turnContext: TurnContext) => Promise { + + return async (turnContext: TurnContext) => { + + const dialogSet = new DialogSet(dialogState); + targetDialogs.forEach(targetDialog => dialogSet.add(targetDialog)); + + const dialogContext = await dialogSet.createContext(turnContext); + this.dialogTurnResult = await dialogContext.continueDialog(); + if (this.dialogTurnResult.status === DialogTurnStatus.empty) { + this.dialogTurnResult = await dialogContext.beginDialog(targetDialogs[0].id, initialDialogOptions); + } + }; + } + + private addUserMiddlewares(middlewares: Middleware[]): void { + if (middlewares != null) { + middlewares.forEach((middleware) => { + this._testAdapter.use(middleware); + }); + } + } + +} diff --git a/packages/botkit/tests/Core.tests.js b/packages/botkit/tests/Core.tests.js index e1287348b..c10a0427e 100644 --- a/packages/botkit/tests/Core.tests.js +++ b/packages/botkit/tests/Core.tests.js @@ -4,6 +4,6 @@ const { Botkit } = require('../'); describe('Botkit', function() { it('should create a Botkit controller', function () { - assert((new Botkit({}) instanceof Botkit), 'Botkit is wrong type'); + assert((new Botkit({ disable_webserver: true }) instanceof Botkit), 'Botkit is wrong type'); }); }); diff --git a/packages/botkit/tests/Dialog.tests.js b/packages/botkit/tests/Dialog.tests.js new file mode 100644 index 000000000..1995acf3d --- /dev/null +++ b/packages/botkit/tests/Dialog.tests.js @@ -0,0 +1,34 @@ +const assert = require('assert'); +const { Botkit, BotkitTestClient, BotkitConversation } = require('../'); + +let bot; + +describe('Botkit dialog', function() { + beforeEach(async () => { + bot = new Botkit(); + }); + + it('should follow a dialog', async function () { + const introDialog = new BotkitConversation('introduction', bot); + introDialog.ask({ + text: 'You can say Ok', + quick_replies: [{ + title: 'Ok', + payload: 'Ok' + }], + }, [], 'continue'); + bot.addDialog(introDialog); + + // set up a test client + const client = new BotkitTestClient('test', bot, 'introduction'); + + // Get details for the reply + const quickreply_reply = await client.sendActivity(); + assert(quickreply_reply.text === 'You can say Ok'); + assert(quickreply_reply.channelData.quick_replies[0].title === 'Ok'); + }); + + afterEach(async () => { + await bot.shutdown(); + }); +});