botium-core 1.14.9 → 1.14.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botium-core",
3
- "version": "1.14.9",
3
+ "version": "1.14.10",
4
4
  "description": "The Selenium for Chatbots",
5
5
  "main": "index.js",
6
6
  "module": "dist/botium-es.js",
@@ -63,6 +63,7 @@
63
63
  "socketio-auth": "^0.1.1",
64
64
  "swagger-jsdoc": "^6.2.8",
65
65
  "swagger-ui-express": "^5.0.0",
66
+ "tinyglobby": "^0.2.10",
66
67
  "uuid": "^9.0.1",
67
68
  "word-error-rate": "0.0.7",
68
69
  "write-yaml": "^1.0.0",
@@ -277,6 +277,10 @@ class Convo {
277
277
  let skipTranscriptStep = false
278
278
  let conditionalGroupId = null
279
279
  let conditionMetInGroup = false
280
+ let skipOptionalStep = false
281
+ // If there are optional step(s) in the conversation, and the message from the bot fails on each optional bot step(s) and/or mandatory bot step, then we have an unexpected message.
282
+ // So in this case an unexpected error should be shown instead of the latest assertion error.
283
+ let optionalStepAssertionError = false
280
284
  let globalConvoStepParameters = container.caps[Capabilities.SCRIPTING_CONVO_STEP_PARAMETERS] || {}
281
285
  let retryBotMessageTimeoutEnd = null
282
286
  let retryBotMessageConvoId = null
@@ -284,6 +288,13 @@ class Convo {
284
288
  for (let i = 0; i < this.conversation.length; i = (retryBotMessageDropBotResponse ? i : i + 1)) {
285
289
  retryBotMessageDropBotResponse = false
286
290
  const convoStep = this.conversation[i]
291
+ if (!convoStep.optional) {
292
+ skipOptionalStep = false
293
+ }
294
+ if (convoStep.optional && skipOptionalStep) {
295
+ // If there are multiple optional steps, and the previous optional step was timeout, then the next optional step should be skipped to prevent too long convo run with multiple timeout.
296
+ continue
297
+ }
287
298
  const rawConvoStepParameters = convoStep.logicHooks.find(lh => lh.name === 'CONVO_STEP_PARAMETERS')?.args
288
299
  let convoStepParameters = {}
289
300
  if (rawConvoStepParameters && rawConvoStepParameters.length) {
@@ -426,7 +437,8 @@ class Convo {
426
437
  } catch (err) {
427
438
  transcriptStep.botEnd = new Date()
428
439
 
429
- if (convoStep.optional) {
440
+ if (!(err.message.indexOf('Bot did not respond within') < 0) && convoStep.optional) {
441
+ skipOptionalStep = true
430
442
  continue
431
443
  }
432
444
 
@@ -512,6 +524,7 @@ class Convo {
512
524
  }
513
525
  waitForBotSays = false
514
526
  skipTranscriptStep = true
527
+ optionalStepAssertionError = true
515
528
  return true
516
529
  } else if (retryOn) {
517
530
  if (!retryBotMessageTimeoutEnd || retryBotMessageConvoId !== convoStep.stepTag) {
@@ -531,9 +544,18 @@ class Convo {
531
544
  }
532
545
 
533
546
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
534
- assertErrors.push(err)
547
+ if (optionalStepAssertionError) {
548
+ optionalStepAssertionError = false
549
+ assertErrors.push(new BotiumError(`${this.header.name}: Unexpected message.`))
550
+ } else {
551
+ assertErrors.push(err)
552
+ }
535
553
  return false
536
554
  } else {
555
+ if (optionalStepAssertionError) {
556
+ optionalStepAssertionError = false
557
+ throw new BotiumError(`${this.header.name}: Unexpected message.`)
558
+ }
537
559
  throw err
538
560
  }
539
561
  }
@@ -547,6 +569,7 @@ class Convo {
547
569
  if (convoStep.not) {
548
570
  try {
549
571
  this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
572
+ optionalStepAssertionError = false
550
573
  } catch (err) {
551
574
  if (isErrorHandledWithOptionConvoStep(err)) {
552
575
  continue
@@ -555,6 +578,7 @@ class Convo {
555
578
  } else {
556
579
  try {
557
580
  this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
581
+ optionalStepAssertionError = false
558
582
  } catch (err) {
559
583
  if (isErrorHandledWithOptionConvoStep(err)) {
560
584
  continue
@@ -564,6 +588,7 @@ class Convo {
564
588
  } else if (convoStep.sourceData) {
565
589
  try {
566
590
  this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg, convoStepParameters)
591
+ optionalStepAssertionError = false
567
592
  } catch (err) {
568
593
  if (isErrorHandledWithOptionConvoStep(err)) {
569
594
  continue
@@ -574,11 +599,13 @@ class Convo {
574
599
  try {
575
600
  await this.scriptingEvents.assertConvoStep({ convo: this, convoStep, container, scriptingMemory, botMsg, transcript, transcriptStep })
576
601
  await this.scriptingEvents.onBotEnd({ convo: this, convoStep, container, scriptingMemory, botMsg, transcript, transcriptStep })
602
+ optionalStepAssertionError = false
577
603
  } catch (err) {
578
604
  const nextConvoStep = this.conversation[i + 1]
579
605
  if (convoStep.optional && nextConvoStep && nextConvoStep.sender === 'bot') {
580
606
  waitForBotSays = false
581
607
  skipTranscriptStep = true
608
+ optionalStepAssertionError = true
582
609
  continue
583
610
  }
584
611
 
@@ -611,8 +638,17 @@ class Convo {
611
638
  } catch (failErr) {
612
639
  }
613
640
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS] && err instanceof BotiumError) {
614
- assertErrors.push(err)
641
+ if (optionalStepAssertionError) {
642
+ optionalStepAssertionError = false
643
+ assertErrors.push(new BotiumError(`${this.header.name}: Unexpected message.`))
644
+ } else {
645
+ assertErrors.push(err)
646
+ }
615
647
  } else {
648
+ if (optionalStepAssertionError) {
649
+ optionalStepAssertionError = false
650
+ throw new BotiumError(`${this.header.name}: Unexpected message.`)
651
+ }
616
652
  throw failErr
617
653
  }
618
654
  }
@@ -83,9 +83,9 @@ module.exports = class ScriptingProvider {
83
83
  this.utterances = {}
84
84
  this.matchFn = null
85
85
  this.asserters = {}
86
- this.globalAsserter = {}
86
+ this.globalAsserters = {}
87
87
  this.logicHooks = {}
88
- this.globalLogicHook = {}
88
+ this.globalLogicHooks = {}
89
89
  this.userInputs = {}
90
90
  this.partialConvos = {}
91
91
  this.scriptingMemories = []
@@ -319,8 +319,9 @@ module.exports = class ScriptingProvider {
319
319
  }
320
320
  }
321
321
 
322
- const convoAsserter = asserters
323
- .filter(a => this.asserters[a.name][asserterType])
322
+ const localAsserters = (asserters || []).filter(a => this.asserters[a.name][asserterType])
323
+
324
+ const convoStepPromises = localAsserters
324
325
  .map(a => ({
325
326
  asserter: a,
326
327
  promise: callAsserter(a, this.asserters[a.name], {
@@ -335,8 +336,12 @@ module.exports = class ScriptingProvider {
335
336
  }))
336
337
  .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
337
338
 
338
- const globalAsserter = Object.values(this.globalAsserter)
339
+ const globalAsserters = Object.keys(this.globalAsserters)
340
+ .filter(name => localAsserters.map(a => a.name).indexOf(name) < 0)
341
+ .reduce((agg, name) => [...agg, this.globalAsserters[name]], [])
339
342
  .filter(a => a[asserterType])
343
+
344
+ const globalPromises = globalAsserters
340
345
  .map(a => ({
341
346
  asserter: a,
342
347
  promise: p(this.retryHelperAsserter, () => a[asserterType]({
@@ -351,7 +356,7 @@ module.exports = class ScriptingProvider {
351
356
  }))
352
357
  .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
353
358
 
354
- const allPromises = [...convoAsserter, ...globalAsserter]
359
+ const allPromises = [...convoStepPromises, ...globalPromises]
355
360
  if (this.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
356
361
  return Promise.allSettled(allPromises).then((results) => {
357
362
  const rejected = results.filter(result => result.status === 'rejected').map(result => result.reason)
@@ -372,8 +377,7 @@ module.exports = class ScriptingProvider {
372
377
  throw Error(`Unknown hookType ${hookType}`)
373
378
  }
374
379
 
375
- const localHooks = (logicHooks || [])
376
- .filter(l => this.logicHooks[l.name][hookType])
380
+ const localHooks = (logicHooks || []).filter(l => this.logicHooks[l.name][hookType])
377
381
 
378
382
  const convoStepPromises = localHooks
379
383
  .map(l => p(this.retryHelperLogicHook, () => this.logicHooks[l.name][hookType]({
@@ -386,7 +390,10 @@ module.exports = class ScriptingProvider {
386
390
  ...rest
387
391
  })))
388
392
 
389
- const globalHooks = Object.values(this.globalLogicHook).filter(l => l[hookType])
393
+ const globalHooks = Object.keys(this.globalLogicHooks)
394
+ .filter(name => localHooks.map(l => l.name).indexOf(name) < 0)
395
+ .reduce((agg, name) => [...agg, this.globalLogicHooks[name]], [])
396
+ .filter(l => l[hookType])
390
397
  const globalPromises = globalHooks.map(l => p(this.retryHelperLogicHook, () => l[hookType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
391
398
 
392
399
  const allPromises = [...convoStepPromises, ...globalPromises]
@@ -499,9 +506,9 @@ module.exports = class ScriptingProvider {
499
506
 
500
507
  const logicHookUtils = new LogicHookUtils({ buildScriptContext: this._buildScriptContext(), caps: this.caps })
501
508
  this.asserters = logicHookUtils.asserters
502
- this.globalAsserter = logicHookUtils.getGlobalAsserter()
509
+ this.globalAsserters = logicHookUtils.getGlobalAsserters()
503
510
  this.logicHooks = logicHookUtils.logicHooks
504
- this.globalLogicHook = logicHookUtils.getGlobalLogicHook()
511
+ this.globalLogicHooks = logicHookUtils.getGlobalLogicHooks()
505
512
  this.userInputs = logicHookUtils.userInputs
506
513
  }
507
514
 
@@ -24,9 +24,9 @@ const _ = require('lodash')
24
24
  module.exports = class LogicHookUtils {
25
25
  constructor ({ buildScriptContext, caps }) {
26
26
  this.asserters = {}
27
- this.globalAsserters = []
27
+ this.globalAsserterNames = []
28
28
  this.logicHooks = {}
29
- this.globalLogicHooks = []
29
+ this.globalLogicHookNames = []
30
30
  this.userInputs = {}
31
31
  this.buildScriptContext = buildScriptContext
32
32
  this.caps = caps
@@ -64,7 +64,7 @@ module.exports = class LogicHookUtils {
64
64
  }
65
65
  this.asserters[asserter.ref] = this._loadClass(asserter, 'asserter')
66
66
  if (asserter.global) {
67
- this.globalAsserters.push(asserter.ref)
67
+ this.globalAsserterNames.push(asserter.ref)
68
68
  }
69
69
  })
70
70
  }
@@ -77,7 +77,7 @@ module.exports = class LogicHookUtils {
77
77
  }
78
78
  this.logicHooks[logicHook.ref] = this._loadClass(logicHook, 'logichook')
79
79
  if (logicHook.global) {
80
- this.globalLogicHooks.push(logicHook.ref)
80
+ this.globalLogicHookNames.push(logicHook.ref)
81
81
  }
82
82
  })
83
83
  }
@@ -92,14 +92,12 @@ module.exports = class LogicHookUtils {
92
92
  })
93
93
  }
94
94
 
95
- getGlobalAsserter () {
96
- return this.globalAsserters
97
- .map(name => this.asserters[name])
95
+ getGlobalAsserters () {
96
+ return this.globalAsserterNames.reduce((agg, name) => ({ ...agg, [name]: this.asserters[name] }), {})
98
97
  }
99
98
 
100
- getGlobalLogicHook () {
101
- return this.globalLogicHooks
102
- .map(name => this.logicHooks[name])
99
+ getGlobalLogicHooks () {
100
+ return this.globalLogicHookNames.reduce((agg, name) => ({ ...agg, [name]: this.logicHooks[name] }), {})
103
101
  }
104
102
 
105
103
  _loadClass ({ src, ref, args }, hookType) {
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const globby = require('globby')
3
+ const globSync = require('tinyglobby').globSync
4
4
  const request = require('request')
5
5
  const mime = require('mime-types')
6
6
  const url = require('url')
@@ -161,7 +161,7 @@ module.exports = class MediaInput {
161
161
  const baseDir = this._getBaseDir(convo)
162
162
  return args.reduce((e, arg) => {
163
163
  if (this._isWildcard(arg)) {
164
- const mediaFiles = globby.sync(arg, { cwd: baseDir, gitignore: true })
164
+ const mediaFiles = globSync(arg, { cwd: baseDir })
165
165
  mediaFiles.forEach(mf => {
166
166
  e.push({
167
167
  name: 'MEDIA',
@@ -49,6 +49,57 @@ const echoConnectorMultipleBotMessages = ({ queueBotSays }) => {
49
49
  }
50
50
  }
51
51
 
52
+ const echoConnectorMultipleBotMessagesSkipAnOptional = ({ queueBotSays }) => {
53
+ return {
54
+ UserSays (msg) {
55
+ const botMsg = { sender: 'bot', sourceData: msg.sourceData, messageText: msg.messageText }
56
+ if (msg.messageText === 'Welcome') {
57
+ botMsg.messageText = 'Welcome'
58
+ queueBotSays(botMsg)
59
+
60
+ setTimeout(() => {
61
+ botMsg.messageText = ''
62
+ botMsg.buttons = [
63
+ { text: 'First Button' },
64
+ { text: 'Second Button' }
65
+ ]
66
+ queueBotSays(botMsg)
67
+ }, 200)
68
+ } else {
69
+ queueBotSays(botMsg)
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ const echoConnectorMultipleBotMessagesAssertionFailOnOptional = ({ queueBotSays }) => {
76
+ return {
77
+ UserSays (msg) {
78
+ const botMsg = { sender: 'bot', sourceData: msg.sourceData, messageText: msg.messageText }
79
+ if (msg.messageText === 'Welcome') {
80
+ botMsg.messageText = 'Welcome'
81
+ queueBotSays(botMsg)
82
+
83
+ setTimeout(() => {
84
+ botMsg.messageText = 'Fail on this:'
85
+ queueBotSays(botMsg)
86
+ }, 200)
87
+
88
+ setTimeout(() => {
89
+ botMsg.messageText = ''
90
+ botMsg.buttons = [
91
+ { text: 'First Button' },
92
+ { text: 'Second Button' }
93
+ ]
94
+ queueBotSays(botMsg)
95
+ }, 200)
96
+ } else {
97
+ queueBotSays(botMsg)
98
+ }
99
+ }
100
+ }
101
+ }
102
+
52
103
  describe('convo.transcript', function () {
53
104
  beforeEach(async function () {
54
105
  const myCaps = {
@@ -92,6 +143,26 @@ describe('convo.transcript', function () {
92
143
  this.compilerMultipleBotmessages = this.driverMultipleBotmessages.BuildCompiler()
93
144
  this.containerMultipleBotmessages = await this.driverMultipleBotmessages.Build()
94
145
  await this.containerMultipleBotmessages.Start()
146
+
147
+ const myCapsMultipleBotMessagesSkipAnOptional = {
148
+ [Capabilities.PROJECTNAME]: 'convo.transcript',
149
+ [Capabilities.CONTAINERMODE]: echoConnectorMultipleBotMessagesSkipAnOptional,
150
+ [Capabilities.SCRIPTING_FORCE_BOT_CONSUMED]: true
151
+ }
152
+ this.driverMultipleBotmessagesSkipAnOptional = new BotDriver(myCapsMultipleBotMessagesSkipAnOptional)
153
+ this.compilerMultipleBotmessagesSkipAnOptional = this.driverMultipleBotmessagesSkipAnOptional.BuildCompiler()
154
+ this.containerMultipleBotmessagesSkipAnOptional = await this.driverMultipleBotmessagesSkipAnOptional.Build()
155
+ await this.containerMultipleBotmessagesSkipAnOptional.Start()
156
+
157
+ const myCapsMultipleBotMessagesAssertionFailOnOptional = {
158
+ [Capabilities.PROJECTNAME]: 'convo.transcript',
159
+ [Capabilities.CONTAINERMODE]: echoConnectorMultipleBotMessagesAssertionFailOnOptional,
160
+ [Capabilities.SCRIPTING_FORCE_BOT_CONSUMED]: true
161
+ }
162
+ this.driverMultipleBotmessagesAssertionFailOnOptional = new BotDriver(myCapsMultipleBotMessagesAssertionFailOnOptional)
163
+ this.compilerMultipleBotmessagesAssertionFailOnOptional = this.driverMultipleBotmessagesAssertionFailOnOptional.BuildCompiler()
164
+ this.containerMultipleBotmessagesAssertionFailOnOptional = await this.driverMultipleBotmessagesAssertionFailOnOptional.Build()
165
+ await this.containerMultipleBotmessagesAssertionFailOnOptional.Start()
95
166
  })
96
167
  afterEach(async function () {
97
168
  await this.container.Stop()
@@ -189,13 +260,34 @@ describe('convo.transcript', function () {
189
260
  assert.isDefined(transcript)
190
261
  assert.equal(transcript.steps.length, 6)
191
262
  })
263
+ it('should provide transcript optional multiple bot steps on skip an optional bot messages', async function () {
264
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'welcome_multiple_botsteps_opt.convo.txt')
265
+ assert.equal(this.compiler.convos.length, 1)
266
+
267
+ const transcript = await this.compiler.convos[0].Run(this.containerMultipleBotmessagesSkipAnOptional)
268
+ assert.isDefined(transcript)
269
+ assert.equal(transcript.steps.length, 5)
270
+ })
271
+ it('should provide transcript optional multiple bot steps assertion fail on optional bot messages', async function () {
272
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'welcome_multiple_botsteps_opt.convo.txt')
273
+ assert.equal(this.compiler.convos.length, 1)
274
+
275
+ try {
276
+ await this.compiler.convos[0].Run(this.containerMultipleBotmessagesAssertionFailOnOptional)
277
+ assert.fail('expected error')
278
+ } catch (err) {
279
+ assert.isDefined(err.transcript)
280
+ assert.equal(err.transcript.steps.length, 3)
281
+ assert.isTrue(err.message.includes('Unexpected message'))
282
+ }
283
+ })
192
284
  it('should provide transcript optional multiple bot steps on not getting all bot messages', async function () {
193
285
  this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'welcome_multiple_botsteps_opt.convo.txt')
194
286
  assert.equal(this.compiler.convos.length, 1)
195
287
 
196
288
  const transcript = await this.compiler.convos[0].Run(this.container)
197
289
  assert.isDefined(transcript)
198
- assert.equal(transcript.steps.length, 6)
290
+ assert.equal(transcript.steps.length, 5)
199
291
  })
200
292
  it('should include pause in transcript steps', async function () {
201
293
  this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '2stepsWithPause.convo.txt')
@@ -0,0 +1,105 @@
1
+ const Constants = require('../../../src/scripting/Constants')
2
+ const assert = require('chai').assert
3
+ const BotDriver = require('../../..').BotDriver
4
+ const Capabilities = require('../../..').Capabilities
5
+
6
+ const echoConnector = () => ({ queueBotSays }) => {
7
+ return {
8
+ UserSays (msg) {
9
+ const botMsg = { sender: 'bot', sourceData: msg.sourceData, messageText: msg.messageText }
10
+ queueBotSays(botMsg)
11
+ }
12
+ }
13
+ }
14
+
15
+ const buildDriver = async (mergeCaps) => {
16
+ const myCaps = Object.assign({
17
+ [Capabilities.PROJECTNAME]: 'convo.localvsglobal',
18
+ [Capabilities.CONTAINERMODE]: echoConnector()
19
+ }, mergeCaps)
20
+
21
+ const result = {}
22
+ result.driver = new BotDriver(myCaps)
23
+ result.compiler = result.driver.BuildCompiler()
24
+ result.container = await result.driver.Build()
25
+ return result
26
+ }
27
+
28
+ const convoScriptAsserters = `
29
+ LOCALVSGLOBAL
30
+
31
+ #me
32
+ hello
33
+
34
+ #bot
35
+
36
+ #me
37
+ hello 2
38
+
39
+ #bot
40
+ MYASSERTER
41
+ `
42
+
43
+ const convoScriptHooks = `
44
+ LOCALVSGLOBAL
45
+
46
+ #me
47
+ hello
48
+
49
+ #bot
50
+
51
+ #me
52
+ hello 2
53
+
54
+ #bot
55
+ MYHOOK
56
+ `
57
+
58
+ describe('Using local and global hooks together', function () {
59
+ it('should use local and global asserter', async function () {
60
+ let localAssertionCount = 0
61
+ let globalAssertionCount = 0
62
+
63
+ const { compiler, container } = await buildDriver({
64
+ [Capabilities.ASSERTERS]: [{
65
+ ref: 'MYASSERTER',
66
+ src: {
67
+ assertConvoStep: ({ isGlobal }) => {
68
+ if (isGlobal) globalAssertionCount++
69
+ else localAssertionCount++
70
+ return Promise.resolve()
71
+ }
72
+ },
73
+ global: true
74
+ }]
75
+ })
76
+
77
+ compiler.ReadScriptFromBuffer(Buffer.from(convoScriptAsserters), Constants.SCRIPTING_FORMAT_TXT, Constants.SCRIPTING_TYPE_CONVO)
78
+ await compiler.convos[0].Run(container)
79
+ assert.equal(localAssertionCount, 1)
80
+ assert.equal(globalAssertionCount, 1)
81
+ })
82
+ it('should use local and global logic hooks', async function () {
83
+ let localHookCount = 0
84
+ let globalHookCount = 0
85
+
86
+ const { compiler, container } = await buildDriver({
87
+ [Capabilities.LOGIC_HOOKS]: [{
88
+ ref: 'MYHOOK',
89
+ src: {
90
+ onBotEnd: ({ isGlobal }) => {
91
+ if (isGlobal) globalHookCount++
92
+ else localHookCount++
93
+ return Promise.resolve()
94
+ }
95
+ },
96
+ global: true
97
+ }]
98
+ })
99
+
100
+ compiler.ReadScriptFromBuffer(Buffer.from(convoScriptHooks), Constants.SCRIPTING_FORMAT_TXT, Constants.SCRIPTING_TYPE_CONVO)
101
+ await compiler.convos[0].Run(container)
102
+ assert.equal(localHookCount, 1)
103
+ assert.equal(globalHookCount, 1)
104
+ })
105
+ })