UNPKG

node-nlp

Version:

Library for NLU (Natural Language Understanding) done in Node.js

422 lines 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * @module botbuilder */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ // tslint:disable-next-line:no-require-imports const assert = require("assert"); 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 logic The bots logic that's under test. * @param template (Optional) activity containing default values to assign to all test messages received. */ constructor(logic, template, sendTraceActivities) { super(); this.logic = logic; /** * @private * INTERNAL: used to drive the promise chain forward when running tests. */ this.activityBuffer = []; /** * List of updated activities passed to the adapter which can be inspected after the current * turn completes. * * @remarks * This example shows how to test that expected updates have been preformed: * * ```JavaScript * adapter.test('update', '1 updated').then(() => { * assert(adapter.updatedActivities.length === 1); * assert(adapter.updatedActivities[0].id === '12345'); * done(); * }); * ``` */ this.updatedActivities = []; /** * List of deleted activities passed to the adapter which can be inspected after the current * turn completes. * * @remarks * This example shows how to test that expected deletes have been preformed: * * ```JavaScript * adapter.test('delete', '1 deleted').then(() => { * assert(adapter.deletedActivities.length === 1); * assert(adapter.deletedActivities[0].activityId === '12345'); * done(); * }); * ``` */ this.deletedActivities = []; this.sendTraceActivities = false; this.nextId = 0; this.sendTraceActivities = sendTraceActivities || false; this.template = Object.assign({ channelId: 'test', serviceUrl: 'https://test.com', from: { id: 'user', name: 'User1' }, recipient: { id: 'bot', name: 'Bot' }, conversation: { id: 'Convo1' } }, template); } /** * @private * INTERNAL: called by the logic under test to send a set of activities. These will be buffered * to the current `TestFlow` instance for comparison against the expected results. * @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) { const responses = activities .filter((a) => this.sendTraceActivities || a.type !== 'trace') .map((activity) => { this.activityBuffer.push(activity); return { id: (this.nextId++).toString() }; }); return Promise.resolve(responses); } /** * @private * INTERNAL: called by the logic under test to replace an existing activity. These are simply * pushed onto an [updatedActivities](#updatedactivities) array for inspection after the turn * completes. * @param context Context object for the current turn of conversation with the user. * @param activity Activity being updated. */ updateActivity(context, activity) { this.updatedActivities.push(activity); return Promise.resolve(); } /** * @private * INTERNAL: called by the logic under test to delete an existing activity. These are simply * pushed onto a [deletedActivities](#deletedactivities) array for inspection after the turn * completes. * @param context Context object for the current turn of conversation with the user. * @param reference `ConversationReference` for activity being deleted. */ deleteActivity(context, reference) { this.deletedActivities.push(reference); return Promise.resolve(); } /** * The `TestAdapter` doesn't implement `continueConversation()` and will return an error if it's * called. */ continueConversation(reference, logic) { return Promise.reject(new Error(`not implemented`)); } /** * @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) { // Initialize request // tslint:disable-next-line:prefer-object-spread const request = Object.assign({}, this.template, typeof activity === 'string' ? { type: botframework_schema_1.ActivityTypes.Message, text: activity } : activity); if (!request.type) { request.type = botframework_schema_1.ActivityTypes.Message; } if (!request.id) { request.id = (this.nextId++).toString(); } // Create context object and run middleware const context = new turnContext_1.TurnContext(this, request); return this.runMiddleware(context, this.logic); } /** * 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. */ send(userSays) { return new TestFlow(this.receiveActivity(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`. */ 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`. */ 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)); } /** * 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. */ isReply(activity) { if (activity.from && activity.from.role) { return activity.from.role && activity.from.role.toLocaleLowerCase() === 'bot'; } else { return false; } } } exports.TestAdapter = TestAdapter; /** * 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. */ constructor(previous, adapter) { this.previous = previous; this.adapter = adapter; } /** * 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`. */ 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. */ send(userSays) { return new TestFlow(this.previous.then(() => this.adapter.receiveActivity(userSays)), this.adapter); } /** * 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`. */ assertReply(expected, description, timeout) { function defaultInspector(reply, description2) { if (typeof expected === 'object') { validateActivity(reply, expected); } else { assert.equal(reply.type, botframework_schema_1.ActivityTypes.Message, `${description2} type === '${reply.type}'. `); assert.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.activityBuffer.length > 0) { // Activity received const reply = adapter.activityBuffer.shift(); try { inspector(reply, description); } catch (err) { reject(err); } resolve(); } else { setTimeout(waitForActivity, 5); } } waitForActivity(); }); }), this.adapter); } /** * 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`. */ assertReplyOneOf(candidates, description, timeout) { return this.assertReply((activity, description2) => { for (const candidate of candidates) { if (activity.text === candidate) { return; } } assert.fail(`TestAdapter.assertReplyOneOf(): ${description2 || ''} FAILED, Expected one of :${JSON.stringify(candidates)}`); }, description, timeout); } /** * Inserts a delay before continuing. * @param ms ms to wait */ delay(ms) { return new TestFlow(this.previous.then(() => { return new Promise((resolve, reject) => { setTimeout(resolve, ms); }); }), this.adapter); } /** * Adds a `then()` step to the tests promise chain. * @param onFulfilled Code to run if the test is currently passing. */ then(onFulfilled) { return new TestFlow(this.previous.then(onFulfilled), this.adapter); } /** * Adds a `catch()` clause to the tests promise chain. * @param onRejected Code to run if the test has thrown an error. */ catch(onRejected) { return new TestFlow(this.previous.catch(onRejected), this.adapter); } /** * Start the test sequence, returning a promise to await */ 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.equal(activity[prop], expected[prop]); }); } /** * @private * Does a shallow comparison of: * - type * - text * - speak * - suggestedActions */ function validateTranscriptActivity(activity, expected, description) { assert.equal(activity.type, expected.type, `failed "type" assert on ${description}`); assert.equal(activity.text, expected.text, `failed "text" assert on ${description}`); assert.equal(activity.speak, expected.speak, `failed "speak" assert on ${description}`); assert.deepEqual(activity.suggestedActions, expected.suggestedActions, `failed "suggestedActions" assert on ${description}`); } //# sourceMappingURL=testAdapter.js.map