UNPKG

botbuilder-core

Version:

Core components for Microsoft Bot Builder. Components in this library can run either in a browser or on the server.

898 lines 38.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestFlow = exports.TestAdapter = void 0; /** * @module botbuilder */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ // tslint:disable-next-line:no-require-imports const assert_1 = __importDefault(require("assert")); const uuid_1 = require("uuid"); const botframework_schema_1 = require("botframework-schema"); const botAdapter_1 = require("./botAdapter"); const turnContext_1 = require("./turnContext"); /** * Test adapter used for unit tests. This adapter can be used to simulate sending messages from the * user to the bot. * * @remarks * The following example sets up the test adapter and then executes a simple test: * * ```JavaScript * const { TestAdapter } = require('botbuilder'); * * const adapter = new TestAdapter(async (context) => { * await context.sendActivity(`Hello World`); * }); * * adapter.test(`hi`, `Hello World`) * .then(() => done()); * ``` */ class TestAdapter extends botAdapter_1.BotAdapter { /** * Creates a new TestAdapter instance. * * @param logicOrConversation The bots logic that's under test. * @param template (Optional) activity containing default values to assign to all test messages received. * @param sendTraceActivity Indicates whether the adapter should add to its queue any trace activities generated by the bot. */ constructor(logicOrConversation, template, sendTraceActivity = false) { super(); /** * Gets or sets the locale for the conversation. */ this.locale = 'en-us'; /** * Gets the queue of responses from the bot. */ this.activeQueue = []; this._sendTraceActivity = false; this._nextId = 0; this.ExceptionExpected = 'ExceptionExpected'; this._userTokens = []; this._magicCodes = []; this.exchangeableTokens = {}; this._sendTraceActivity = sendTraceActivity; this.template = template || {}; if (logicOrConversation) { if (typeof logicOrConversation === 'function') { this._logic = logicOrConversation; this.conversation = TestAdapter.createConversation('Convo1'); } else { this.conversation = logicOrConversation; } } else { this.conversation = TestAdapter.createConversation('Convo1'); } Object.assign(this.conversation, { locale: this.template.locale || this.conversation.locale || this.locale, serviceUrl: this.template.serviceUrl || this.conversation.serviceUrl, channelId: this.template.channelId || this.conversation.channelId, bot: this.template.recipient || this.conversation.bot, user: this.template.from || this.conversation.user, }); } /** * @private * INTERNAL: used to drive the promise chain forward when running tests. */ get activityBuffer() { return this.activeQueue; } /** * Gets a value indicating whether to send trace activities. * * @returns A value indicating whether to send trace activities. */ get enableTrace() { return this._sendTraceActivity; } /** * Sets a value inidicating whether to send trace activities. */ set enableTrace(value) { this._sendTraceActivity = value; } /** * Create a ConversationReference. * * @param name name of the conversation (also id). * @param user name of the user (also id) default: User1. * @param bot name of the bot (also id) default: Bot. * @returns The [ConversationReference](xref:botframework-schema.ConversationReference). */ static createConversation(name, user = 'User1', bot = 'Bot') { const conversationReference = { channelId: botframework_schema_1.Channels.Test, serviceUrl: 'https://test.com', conversation: { isGroup: false, id: name, name: name }, user: { id: user.toLowerCase(), name: user }, bot: { id: bot.toLowerCase(), name: bot }, locale: 'en-us', }; return conversationReference; } /** * Dequeues and returns the next bot response from the activeQueue. * * @returns The next activity in the queue; or undefined, if the queue is empty. */ getNextReply() { if (this.activeQueue.length > 0) { return this.activeQueue.shift(); } return undefined; } /** * Creates a message activity from text and the current conversational context. * * @param text The message text. * @returns An appropriate message activity. */ makeActivity(text) { const activity = { type: botframework_schema_1.ActivityTypes.Message, locale: this.locale, from: this.conversation.user, recipient: this.conversation.bot, conversation: this.conversation.conversation, serviceUrl: this.conversation.serviceUrl, id: (this._nextId++).toString(), text: text, }; return activity; } /** * Processes a message activity from a user. * * @param userSays The text of the user's message. * @param callback The bot logic to invoke. * @returns {Promise<any>} A promise representing the async operation. */ sendTextToBot(userSays, callback) { return this.processActivity(this.makeActivity(userSays), callback); } /** * Receives an activity and runs it through the middleware pipeline. * * @param activity The activity to process. * @param callback The bot logic to invoke. * @returns {Promise<any>} A promise representing the async operation. */ processActivity(activity, callback) { return __awaiter(this, void 0, void 0, function* () { const request = typeof activity === 'string' ? { type: botframework_schema_1.ActivityTypes.Message, text: activity } : activity; request.type = request.type || botframework_schema_1.ActivityTypes.Message; request.channelId = request.channelId || this.conversation.channelId; if (!request.from || request.from.id === 'unknown' || request.from.role === botframework_schema_1.RoleTypes.Bot) { request.from = this.conversation.user; } request.recipient = request.recipient || this.conversation.bot; request.conversation = request.conversation || this.conversation.conversation; request.serviceUrl = request.serviceUrl || this.conversation.serviceUrl; request.id = request.id || (this._nextId++).toString(); request.timestamp = request.timestamp || new Date(); Object.assign(request, this.template); const context = this.createContext(request); if (callback) { return yield this.runMiddleware(context, callback); } else if (this._logic) { return yield this.runMiddleware(context, this._logic); } }); } /** * @private * Sends activities to the conversation. * @param context Context object for the current turn of conversation with the user. * @param activities Set of activities sent by logic under test. */ sendActivities(context, activities) { return __awaiter(this, void 0, void 0, function* () { if (!context) { throw new Error('TurnContext cannot be null.'); } if (!activities) { throw new Error('Activities cannot be null.'); } if (activities.length == 0) { throw new Error('Expecting one or more activities, but the array was empty.'); } const responses = []; for (let i = 0; i < activities.length; i++) { const activity = activities[i]; if (!activity.id) { activity.id = (0, uuid_1.v4)(); } if (!activity.timestamp) { activity.timestamp = new Date(); } if (activity.type === 'delay') { const delayMs = parseInt(activity.value); yield new Promise((resolve) => setTimeout(resolve, delayMs)); } else if (activity.type === botframework_schema_1.ActivityTypes.Trace) { if (this._sendTraceActivity) { this.activeQueue.push(activity); } } else { this.activeQueue.push(activity); } responses.push({ id: activity.id }); } return responses; }); } /** * @private * Replaces an existing activity in the activeQueue. * @param context Context object for the current turn of conversation with the user. * @param activity Activity being updated. * @returns promise representing async operation */ updateActivity(context, activity) { if (activity.id) { const idx = this.activeQueue.findIndex((a) => a.id === activity.id); if (idx !== -1) { this.activeQueue.splice(idx, 1, activity); } return Promise.resolve({ id: activity.id }); } return Promise.resolve(); } /** * @private * Deletes an existing activity in the activeQueue. * @param context Context object for the current turn of conversation with the user. * @param reference `ConversationReference` for activity being deleted. */ deleteActivity(context, reference) { if (reference.activityId) { const idx = this.activeQueue.findIndex((a) => a.id === reference.activityId); if (idx !== -1) { this.activeQueue.splice(idx, 1); } } return Promise.resolve(); } /** * @private * INTERNAL: called by a `TestFlow` instance to simulate a user sending a message to the bot. * This will cause the adapters middleware pipe to be run and it's logic to be called. * @param activity Text or activity from user. The current conversation reference [template](#template) will be merged the passed in activity to properly address the activity. Fields specified in the activity override fields in the template. */ receiveActivity(activity) { return this.processActivity(activity); } /** * The `TestAdapter` doesn't implement `continueConversation()` and will return an error if it's * called. * * @param _reference A reference to the conversation to continue. * @param _logic The asynchronous method to call after the adapter middleware runs. * @returns {Promise<void>} A promise representing the async operation. */ continueConversation(_reference, _logic) { return Promise.reject(new Error('not implemented')); } /** * Creates a turn context. * * @param request An incoming request body. * @returns The created [TurnContext](xref:botbuilder-core.TurnContext). * @remarks * Override this in a derived class to modify how the adapter creates a turn context. */ createContext(request) { return new turnContext_1.TurnContext(this, request); } /** * Sends something to the bot. This returns a new `TestFlow` instance which can be used to add * additional steps for inspecting the bots reply and then sending additional activities. * * @remarks * This example shows how to send a message and then verify that the response was as expected: * * ```JavaScript * adapter.send('hi') * .assertReply('Hello World') * .then(() => done()); * ``` * @param userSays Text or activity simulating user input. * @returns a new [TestFlow](xref:botbuilder-core.TestFlow) instance which can be used to add additional steps * for inspecting the bots reply and then sending additional activities. */ send(userSays) { return new TestFlow(this.processActivity(userSays), this); } /** * Send something to the bot and expects the bot to return with a given reply. * * @remarks * This is simply a wrapper around calls to `send()` and `assertReply()`. This is such a * common pattern that a helper is provided. * * ```JavaScript * adapter.test('hi', 'Hello World') * .then(() => done()); * ``` * @param userSays Text or activity simulating user input. * @param expected Expected text or activity of the reply sent by the bot. * @param description (Optional) Description of the test case. If not provided one will be generated. * @param _timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ test(userSays, expected, description, _timeout) { return this.send(userSays).assertReply(expected, description); } /** * Test a list of activities. * * @remarks * Each activity with the "bot" role will be processed with assertReply() and every other * activity will be processed as a user message with send(). * @param activities Array of activities. * @param description (Optional) Description of the test case. If not provided one will be generated. * @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ testActivities(activities, description, timeout) { if (!activities) { throw new Error('Missing array of activities'); } const activityInspector = (expected) => (actual, description2) => validateTranscriptActivity(actual, expected, description2); // Chain all activities in a TestFlow, check if its a user message (send) or a bot reply (assert) return activities.reduce((flow, activity) => { // tslint:disable-next-line:prefer-template const assertDescription = `reply ${description ? ' from ' + description : ''}`; return this.isReply(activity) ? flow.assertReply(activityInspector(activity, description), assertDescription, timeout) : flow.send(activity); }, new TestFlow(Promise.resolve(), this)); } /** * Adds a fake user token so it can later be retrieved. * * @param connectionName The connection name. * @param channelId The channel id. * @param userId The user id. * @param token The token to store. * @param magicCode (Optional) The optional magic code to associate with this token. */ addUserToken(connectionName, channelId, userId, token, magicCode) { const key = new UserToken(); key.channelId = channelId; key.connectionName = connectionName; key.userId = userId; key.token = token; if (!magicCode) { this._userTokens.push(key); } else { const mc = new TokenMagicCode(); mc.key = key; mc.magicCode = magicCode; this._magicCodes.push(mc); } } /** * Asynchronously retrieves the token status for each configured connection for the given user. * In testAdapter, retrieves tokens which were previously added via addUserToken. * * @param context The context object for the turn. * @param userId The ID of the user to retrieve the token status for. * @param includeFilter Optional. A comma-separated list of connection's to include. If present, * the `includeFilter` parameter limits the tokens this method returns. * @param _oAuthAppCredentials AppCredentials for OAuth. * @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved. */ getTokenStatus(context, userId, includeFilter, _oAuthAppCredentials) { return __awaiter(this, void 0, void 0, function* () { if (!context || !context.activity) { throw new Error('testAdapter.getTokenStatus(): context with activity is required'); } if (!userId && (!context.activity.from || !context.activity.from.id)) { throw new Error('testAdapter.getTokenStatus(): missing userId, from or from.id'); } const filter = includeFilter ? includeFilter.split(',') : undefined; if (!userId) { userId = context.activity.from.id; } return this._userTokens .filter((x) => x.channelId === context.activity.channelId && x.userId === userId && (!filter || filter.includes(x.connectionName))) .map((token) => ({ ConnectionName: token.connectionName, HasToken: true, ServiceProviderDisplayName: token.connectionName, })); }); } /** * Retrieves the OAuth token for a user that is in a sign-in flow. * * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. * @param magicCode (Optional) Optional user entered code to validate. * @returns The OAuth token for a user that is in a sign-in flow. */ getUserToken(context, connectionName, magicCode) { return __awaiter(this, void 0, void 0, function* () { const key = new UserToken(); key.channelId = context.activity.channelId; key.connectionName = connectionName; key.userId = context.activity.from.id; if (magicCode) { const magicCodeRecord = this._magicCodes.find((item) => key.equalsKey(item.key) && item.magicCode === magicCode); if (magicCodeRecord) { // move the token to long term dictionary this.addUserToken(connectionName, key.channelId, key.userId, magicCodeRecord.key.token); // remove from the magic code list const idx = this._magicCodes.indexOf(magicCodeRecord); this._magicCodes.splice(idx, 1); } } const userToken = this._userTokens.find((token) => key.equalsKey(token)); return userToken && Object.assign({ expiration: undefined }, userToken); }); } /** * Signs the user out with the token server. * * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. * @param userId User ID to sign out. */ signOutUser(context, connectionName, userId) { return __awaiter(this, void 0, void 0, function* () { const channelId = context.activity.channelId; userId = userId || context.activity.from.id; this._userTokens = this._userTokens.filter((token) => connectionName && (connectionName !== token.connectionName || channelId !== token.channelId || userId !== token.userId)); }); } /** * Gets a signin link from the token server that can be sent as part of a SigninCard. * * @param context Context for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. * @returns A signin link from the token server that can be sent as part of a SigninCard. */ getSignInLink(context, connectionName) { return __awaiter(this, void 0, void 0, function* () { return `https://fake.com/oauthsignin/${connectionName}/${context.activity.channelId}/${context.activity.from.id}`; }); } /** * Signs the user out with the token server. * * @param _context Context for the current turn of conversation with the user. * @param _connectionName Name of the auth connection to use. * @param _resourceUrls The list of resource URLs to retrieve tokens for. * @returns A Dictionary of resourceUrl to the corresponding TokenResponse. */ getAadTokens(_context, _connectionName, _resourceUrls) { return __awaiter(this, void 0, void 0, function* () { return undefined; }); } /** * Adds a fake exchangeable token so it can be exchanged later. * * @param connectionName Name of the auth connection to use. * @param channelId Channel ID. * @param userId User ID. * @param exchangeableItem Exchangeable token or resource URI. * @param token Token to store. */ addExchangeableToken(connectionName, channelId, userId, exchangeableItem, token) { const key = new ExchangeableToken(); key.channelId = channelId; key.connectionName = connectionName; key.userId = userId; key.exchangeableItem = exchangeableItem; key.token = token; this.exchangeableTokens[key.toKey()] = key; } /** * Gets a sign-in resource. * * @param context [TurnContext](xref:botbuilder-core.TurnContext) for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. * @param userId User ID * @param _finalRedirect Final redirect URL. * @returns A `Promise` with a new [SignInUrlResponse](xref:botframework-schema.SignInUrlResponse) object. */ getSignInResource(context, connectionName, userId, _finalRedirect) { return __awaiter(this, void 0, void 0, function* () { return { signInLink: `https://botframeworktestadapter.com/oauthsignin/${connectionName}/${context.activity.channelId}/${userId}`, tokenExchangeResource: { id: String(Math.random()), providerId: null, uri: `api://${connectionName}/resource`, }, }; }); } /** * Performs a token exchange operation such as for single sign-on. * * @param context [TurnContext](xref:botbuilder-core.TurnContext) for the current turn of conversation with the user. * @param connectionName Name of the auth connection to use. * @param userId User id associated with the token. * @param tokenExchangeRequest Exchange request details, either a token to exchange or a uri to exchange. * @returns If the promise completes, the exchanged token is returned. */ exchangeToken(context, connectionName, userId, tokenExchangeRequest) { return __awaiter(this, void 0, void 0, function* () { const exchangeableValue = tokenExchangeRequest.token ? tokenExchangeRequest.token : tokenExchangeRequest.uri; const key = new ExchangeableToken(); key.channelId = context.activity.channelId; key.connectionName = connectionName; key.exchangeableItem = exchangeableValue; key.userId = userId; const tokenExchangeResponse = this.exchangeableTokens[key.toKey()]; if (tokenExchangeResponse && tokenExchangeResponse.token === this.ExceptionExpected) { throw new Error('Exception occurred during exchanging tokens'); } return tokenExchangeResponse ? { channelId: key.channelId, connectionName: key.connectionName, token: tokenExchangeResponse.token, expiration: null, } : null; }); } /** * Adds an instruction to throw an exception during exchange requests. * * @param connectionName The connection name. * @param channelId The channel id. * @param userId The user id. * @param exchangeableItem The exchangeable token or resource URI. */ throwOnExchangeRequest(connectionName, channelId, userId, exchangeableItem) { const token = new ExchangeableToken(); token.channelId = channelId; token.connectionName = connectionName; token.userId = userId; token.exchangeableItem = exchangeableItem; const key = token.toKey(); token.token = this.ExceptionExpected; this.exchangeableTokens[key] = token; } /** * Indicates if the activity is a reply from the bot (role == 'bot') * * @remarks * Checks to see if the from property and if from.role exists on the Activity before * checking to see who the activity is from. Otherwise returns false by default. * @param activity Activity to check. * @returns True if the activity is a reply from the bot, otherwise, false. */ isReply(activity) { if (activity.from && activity.from.role) { return activity.from.role && activity.from.role.toLocaleLowerCase() === 'bot'; } else { return false; } } } exports.TestAdapter = TestAdapter; class UserToken { equalsKey(rhs) { return (rhs && this.connectionName === rhs.connectionName && this.userId === rhs.userId && this.channelId === rhs.channelId); } } class TokenMagicCode { } class ExchangeableToken extends UserToken { equalsKey(rhs) { return rhs != null && this.exchangeableItem === rhs.exchangeableItem && super.equalsKey(rhs); } toKey() { return this.exchangeableItem; } } /** * Support class for `TestAdapter` that allows for the simple construction of a sequence of tests. * * @remarks * Calling `adapter.send()` or `adapter.test()` will create a new test flow which you can chain * together additional tests using a fluent syntax. * * ```JavaScript * const { TestAdapter } = require('botbuilder'); * * const adapter = new TestAdapter(async (context) => { * if (context.text === 'hi') { * await context.sendActivity(`Hello World`); * } else if (context.text === 'bye') { * await context.sendActivity(`Goodbye`); * } * }); * * adapter.test(`hi`, `Hello World`) * .test(`bye`, `Goodbye`) * .then(() => done()); * ``` */ class TestFlow { /** * @private * INTERNAL: creates a new TestFlow instance. * @param previous Promise chain for the current test sequence. * @param adapter Adapter under tested. * @param callback The bot turn processing logic to test. */ constructor(previous, adapter, callback) { this.previous = previous; this.adapter = adapter; this.callback = callback; } /** * Send something to the bot and expects the bot to return with a given reply. This is simply a * wrapper around calls to `send()` and `assertReply()`. This is such a common pattern that a * helper is provided. * * @param userSays Text or activity simulating user input. * @param expected Expected text or activity of the reply sent by the bot. * @param description (Optional) Description of the test case. If not provided one will be generated. * @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ test(userSays, expected, description, timeout) { return this.send(userSays).assertReply(expected, description || `test("${userSays}", "${expected}")`, timeout); } /** * Sends something to the bot. * * @param userSays Text or activity simulating user input. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ send(userSays) { return new TestFlow(this.previous.then(() => this.adapter.processActivity(userSays, this.callback)), this.adapter, this.callback); } /** * Creates a conversation update activity and process the activity. * * @returns {TestFlow} A new TestFlow object. */ sendConversationUpdate() { return new TestFlow(this.previous.then(() => { var _a; const cu = botframework_schema_1.ActivityEx.createConversationUpdateActivity(); (_a = cu.membersAdded) !== null && _a !== void 0 ? _a : (cu.membersAdded = []); cu.membersAdded.push(this.adapter.conversation.user); return this.adapter.processActivity(cu, this.callback); }), this.adapter, this.callback); } /** * Generates an assertion if the bots response doesn't match the expected text/activity. * * @param expected Expected text or activity from the bot. Can be a callback to inspect the response using custom logic. * @param description (Optional) Description of the test case. If not provided one will be generated. * @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ assertReply(expected, description, timeout) { function defaultInspector(reply, description2) { if (typeof expected === 'object') { validateActivity(reply, expected); } else { assert_1.default.equal(reply.type, botframework_schema_1.ActivityTypes.Message, `${description2} type === '${reply.type}'. `); assert_1.default.equal(reply.text, expected, `${description2} text === "${reply.text}"`); } } if (!description) { description = ''; } const inspector = typeof expected === 'function' ? expected : defaultInspector; return new TestFlow(this.previous.then(() => { // tslint:disable-next-line:promise-must-complete return new Promise((resolve, reject) => { if (!timeout) { timeout = 3000; } const start = new Date().getTime(); const adapter = this.adapter; function waitForActivity() { const current = new Date().getTime(); if (current - start > timeout) { // Operation timed out let expecting; switch (typeof expected) { case 'string': default: expecting = `"${expected.toString()}"`; break; case 'object': expecting = `"${expected.text}`; break; case 'function': expecting = expected.toString(); break; } reject(new Error(`TestAdapter.assertReply(${expecting}): ${description} Timed out after ${current - start}ms.`)); } else if (adapter.activeQueue.length > 0) { // Activity received const reply = adapter.activeQueue.shift(); try { inspector(reply, description); } catch (err) { reject(err); } resolve(); } else { setTimeout(waitForActivity, 5); } } waitForActivity(); }); }), this.adapter, this.callback); } /** * Generates an assertion that the turn processing logic did not generate a reply from the bot, as expected. * * @param description (Optional) Description of the test case. If not provided one will be generated. * @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ assertNoReply(description, timeout) { return new TestFlow(this.previous.then(() => { // tslint:disable-next-line:promise-must-complete return new Promise((resolve) => { if (!timeout) { timeout = 3000; } const start = new Date().getTime(); const adapter = this.adapter; function waitForActivity() { const current = new Date().getTime(); if (current - start > timeout) { // Operation timed out and received no reply resolve(); } else if (adapter.activeQueue.length > 0) { // Activity received const reply = adapter.activeQueue.shift(); assert_1.default.strictEqual(reply, undefined, `${JSON.stringify(reply)} is responded when waiting for no reply: '${description}'`); resolve(); } else { setTimeout(waitForActivity, 5); } } waitForActivity(); }); }), this.adapter, this.callback); } /** * Generates an assertion if the bots response is not one of the candidate strings. * * @param candidates List of candidate responses. * @param description (Optional) Description of the test case. If not provided one will be generated. * @param timeout (Optional) number of milliseconds to wait for a response from bot. Defaults to a value of `3000`. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ assertReplyOneOf(candidates, description, timeout) { return this.assertReply((activity, description2) => { for (const candidate of candidates) { if (activity.text === candidate) { return; } } assert_1.default.fail(`TestAdapter.assertReplyOneOf(): ${description2 || ''} FAILED, Expected one of :${JSON.stringify(candidates)}`); }, description, timeout); } /** * Inserts a delay before continuing. * * @param ms ms to wait. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ delay(ms) { return new TestFlow(this.previous.then(() => { return new Promise((resolve, _reject) => { setTimeout(resolve, ms); }); }), this.adapter, this.callback); } /** * Adds a `then()` step to the tests promise chain. * * @param onFulfilled Code to run if the test is currently passing. * @param onRejected Code to run if the test has thrown an error. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ then(onFulfilled, onRejected) { return new TestFlow(this.previous.then(onFulfilled, onRejected), this.adapter, this.callback); } /** * Adds a finally clause. Note that you can't keep chaining afterwards. * * @param onFinally Code to run after the test chain. * @returns {Promise<void>} A promise representing the async operation. */ finally(onFinally) { return Promise.resolve(this.previous.finally(onFinally)); } /** * Adds a `catch()` clause to the tests promise chain. * * @param onRejected Code to run if the test has thrown an error. * @returns A new [TestFlow](xref:botbuilder-core.TestFlow) object that appends this exchange to the modeled exchange. */ catch(onRejected) { return new TestFlow(this.previous.catch(onRejected), this.adapter, this.callback); } /** * Start the test sequence, returning a promise to await. * * @returns {Promise<void>} A promise representing the async operation. */ startTest() { return this.previous; } } exports.TestFlow = TestFlow; /** * @private * @param activity an activity object to validate * @param expected expected object to validate against */ function validateActivity(activity, expected) { // tslint:disable-next-line:forin Object.keys(expected).forEach((prop) => { assert_1.default.equal(activity[prop], expected[prop]); }); } /** * @private * Does a shallow comparison of: * - type * - text * - speak * - suggestedActions */ function validateTranscriptActivity(activity, expected, description) { assert_1.default.equal(activity.type, expected.type, `failed "type" assert on ${description}`); assert_1.default.equal(activity.text, expected.text, `failed "text" assert on ${description}`); assert_1.default.equal(activity.speak, expected.speak, `failed "speak" assert on ${description}`); assert_1.default.deepEqual(activity.suggestedActions, expected.suggestedActions, `failed "suggestedActions" assert on ${description}`); } //# sourceMappingURL=testAdapter.js.map