botium-core 1.12.5 → 1.12.6
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/dist/botium-cjs.js +22 -14
- package/dist/botium-cjs.js.map +1 -1
- package/dist/botium-es.js +22 -14
- package/dist/botium-es.js.map +1 -1
- package/package.json +1 -1
- package/src/scripting/Convo.js +1 -1
- package/src/scripting/helper.js +19 -12
- package/src/scripting/logichook/asserter/ButtonsAsserter.js +4 -2
- package/src/scripting/logichook/asserter/CardsAsserter.js +4 -2
- package/test/compiler/compilertxt.spec.js +13 -0
- package/test/compiler/convos/txt/convos_args_escape.convo.txt +2 -0
- package/test/convo/convos/continuefailing_timeout.convo.txt +16 -0
- package/test/convo/transcript.spec.js +18 -1
- package/test/scripting/asserters/buttonsAsserter.spec.js +47 -0
- package/test/scripting/asserters/cardsAsserter.spec.js +39 -0
- package/test/scripting/txt/decompile.spec.js +24 -0
package/package.json
CHANGED
package/src/scripting/Convo.js
CHANGED
|
@@ -535,7 +535,7 @@ class Convo {
|
|
|
535
535
|
if (container.caps[Capabilities.SCRIPTING_ENABLE_SKIP_ASSERT_ERRORS]) {
|
|
536
536
|
const transcriptStepErrs = transcript.steps.filter(s => s.err).map(s => s.err)
|
|
537
537
|
if (transcriptStepErrs && transcriptStepErrs.length > 0) {
|
|
538
|
-
transcript.err = botiumErrorFromList([transcriptStepErrs, transcript.err].filter(e => e), {})
|
|
538
|
+
transcript.err = botiumErrorFromList([...transcriptStepErrs.filter(err => err !== transcript.err), transcript.err].filter(e => e), {})
|
|
539
539
|
}
|
|
540
540
|
}
|
|
541
541
|
}
|
package/src/scripting/helper.js
CHANGED
|
@@ -77,6 +77,13 @@ const flatString = (str) => {
|
|
|
77
77
|
return str ? str.split('\n').map(s => s.trim()).join(' ') : ''
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
const _formatAppendArgs = (args) => {
|
|
81
|
+
return args ? ` ${args.map(a => _.isString(a) ? a.replace(/\|/g, '\\|') : `${a}`).join('|')}` : ''
|
|
82
|
+
}
|
|
83
|
+
const _parseArgs = (str) => {
|
|
84
|
+
return (str && str.length > 0 && str.replace(/\\\|/g, '###ESCAPESPLIT###').split('|').map(s => s.replace(/###ESCAPESPLIT###/g, '|').trim())) || []
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
const linesToConvoStep = (lines, sender, context, eol, singleLineMode = false) => {
|
|
81
88
|
if (!validateSender(sender)) throw new Error(`Failed to parse conversation. Section "${sender}" unknown.`)
|
|
82
89
|
|
|
@@ -106,14 +113,14 @@ const linesToConvoStep = (lines, sender, context, eol, singleLineMode = false) =
|
|
|
106
113
|
}
|
|
107
114
|
const name = logicLine.split(' ')[0]
|
|
108
115
|
if (sender !== 'me' && context.IsAsserterValid(name)) {
|
|
109
|
-
const args = (logicLine.length > name.length ? logicLine.substr(name.length + 1)
|
|
116
|
+
const args = (logicLine.length > name.length ? _parseArgs(logicLine.substr(name.length + 1)) : [])
|
|
110
117
|
convoStep.asserters.push({ name, args, not, optional })
|
|
111
118
|
} else if (sender === 'me' && context.IsUserInputValid(name)) {
|
|
112
|
-
const args = (logicLine.length > name.length ? logicLine.substr(name.length + 1)
|
|
119
|
+
const args = (logicLine.length > name.length ? _parseArgs(logicLine.substr(name.length + 1)) : [])
|
|
113
120
|
convoStep.userInputs.push({ name, args })
|
|
114
121
|
textLinesAccepted = false
|
|
115
122
|
} else if (context.IsLogicHookValid(name)) {
|
|
116
|
-
const args = (logicLine.length > name.length ? logicLine.substr(name.length + 1)
|
|
123
|
+
const args = (logicLine.length > name.length ? _parseArgs(logicLine.substr(name.length + 1)) : [])
|
|
117
124
|
convoStep.logicHooks.push({ name, args })
|
|
118
125
|
textLinesAccepted = false
|
|
119
126
|
} else {
|
|
@@ -422,7 +429,7 @@ const convoStepToLines = (step) => {
|
|
|
422
429
|
const lines = []
|
|
423
430
|
if (step.sender === 'me') {
|
|
424
431
|
step.forms && step.forms.filter(form => form.value).forEach((form) => {
|
|
425
|
-
lines.push(`FORM
|
|
432
|
+
lines.push(`FORM${_formatAppendArgs([form.name, form.value])}`)
|
|
426
433
|
})
|
|
427
434
|
if (step.buttons && step.buttons.length > 0) {
|
|
428
435
|
lines.push('BUTTON ' + _decompileButton(step.buttons[0]))
|
|
@@ -432,34 +439,34 @@ const convoStepToLines = (step) => {
|
|
|
432
439
|
lines.push(step.messageText)
|
|
433
440
|
}
|
|
434
441
|
step.userInputs && step.userInputs.forEach((userInput) => {
|
|
435
|
-
lines.push(userInput.name + (userInput.args
|
|
442
|
+
lines.push(userInput.name + _formatAppendArgs(userInput.args))
|
|
436
443
|
})
|
|
437
444
|
step.logicHooks && step.logicHooks.forEach((logicHook) => {
|
|
438
|
-
lines.push(logicHook.name + (logicHook.args
|
|
445
|
+
lines.push(logicHook.name + _formatAppendArgs(logicHook.args))
|
|
439
446
|
})
|
|
440
447
|
} else {
|
|
441
448
|
if (step.messageText) {
|
|
442
449
|
lines.push((step.optional ? '?' : '') + (step.not ? '!' : '') + step.messageText)
|
|
443
450
|
}
|
|
444
|
-
if (step.buttons && step.buttons.length > 0) lines.push('BUTTONS
|
|
445
|
-
if (step.media && step.media.length > 0) lines.push('MEDIA
|
|
451
|
+
if (step.buttons && step.buttons.length > 0) lines.push('BUTTONS' + _formatAppendArgs(step.buttons.filter(b => b.text).map(b => flatString(b.text))))
|
|
452
|
+
if (step.media && step.media.length > 0) lines.push('MEDIA' + _formatAppendArgs(step.media.filter(m => !m.buffer && m.mediaUri).map(m => m.mediaUri)))
|
|
446
453
|
if (step.cards && step.cards.length > 0) {
|
|
447
454
|
step.cards.forEach(c => {
|
|
448
455
|
let cardTexts = []
|
|
449
456
|
if (c.text) cardTexts = cardTexts.concat(_.isArray(c.text) ? c.text : [c.text])
|
|
450
457
|
if (c.subtext) cardTexts = cardTexts.concat(_.isArray(c.subtext) ? c.subtext : [c.subtext])
|
|
451
458
|
if (c.content) cardTexts = cardTexts.concat(_.isArray(c.content) ? c.content : [c.content])
|
|
452
|
-
if (cardTexts.length > 0) lines.push('CARDS
|
|
459
|
+
if (cardTexts.length > 0) lines.push('CARDS' + _formatAppendArgs(cardTexts.map(c => flatString(c))))
|
|
453
460
|
|
|
454
|
-
if (c.buttons && c.buttons.length > 0) lines.push('BUTTONS
|
|
461
|
+
if (c.buttons && c.buttons.length > 0) lines.push('BUTTONS' + _formatAppendArgs(c.buttons.filter(b => b.text).map(b => flatString(b.text))))
|
|
455
462
|
if (c.image && !c.image.buffer && c.image.mediaUri) lines.push('MEDIA ' + c.image.mediaUri)
|
|
456
463
|
})
|
|
457
464
|
}
|
|
458
465
|
step.asserters && step.asserters.forEach((asserter) => {
|
|
459
|
-
lines.push((asserter.optional ? '?' : '') + (asserter.not ? '!' : '') + asserter.name + (asserter.args
|
|
466
|
+
lines.push((asserter.optional ? '?' : '') + (asserter.not ? '!' : '') + asserter.name + _formatAppendArgs(asserter.args))
|
|
460
467
|
})
|
|
461
468
|
step.logicHooks && step.logicHooks.forEach((logicHook) => {
|
|
462
|
-
lines.push(logicHook.name + (logicHook.args
|
|
469
|
+
lines.push(logicHook.name + _formatAppendArgs(logicHook.args))
|
|
463
470
|
})
|
|
464
471
|
}
|
|
465
472
|
return lines.map(l => l.trim())
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const { SCRIPTING_NORMALIZE_TEXT } = require('../../../Capabilities')
|
|
1
2
|
const { BotiumError } = require('../../BotiumError')
|
|
2
3
|
const { buttonsFromMsg } = require('../helpers')
|
|
4
|
+
const { normalizeText } = require('../../helper')
|
|
3
5
|
|
|
4
6
|
module.exports = class ButtonsAsserter {
|
|
5
7
|
constructor (context, caps = {}) {
|
|
@@ -9,14 +11,14 @@ module.exports = class ButtonsAsserter {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
_evalButtons (args, botMsg) {
|
|
12
|
-
const allButtons = buttonsFromMsg(botMsg, true).map(b => b.text)
|
|
14
|
+
const allButtons = buttonsFromMsg(botMsg, true).map(b => b.text).filter(b => b).map(b => normalizeText(b, !!this.caps[SCRIPTING_NORMALIZE_TEXT]))
|
|
13
15
|
if (!args || args.length === 0) {
|
|
14
16
|
return { allButtons, buttonsNotFound: [], buttonsFound: allButtons }
|
|
15
17
|
}
|
|
16
18
|
const buttonsNotFound = []
|
|
17
19
|
const buttonsFound = []
|
|
18
20
|
for (let i = 0; i < (args || []).length; i++) {
|
|
19
|
-
if (allButtons.findIndex(b => this.context.Match(b, args[i])) < 0) {
|
|
21
|
+
if (allButtons.findIndex(b => this.context.Match(b, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT]))) < 0) {
|
|
20
22
|
buttonsNotFound.push(args[i])
|
|
21
23
|
} else {
|
|
22
24
|
buttonsFound.push(args[i])
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const { SCRIPTING_NORMALIZE_TEXT } = require('../../../Capabilities')
|
|
1
2
|
const { BotiumError } = require('../../BotiumError')
|
|
2
3
|
const { cardsFromMsg } = require('../helpers')
|
|
4
|
+
const { normalizeText } = require('../../helper')
|
|
3
5
|
|
|
4
6
|
module.exports = class CardsAsserter {
|
|
5
7
|
constructor (context, caps = {}) {
|
|
@@ -9,14 +11,14 @@ module.exports = class CardsAsserter {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
_evalCards (args, botMsg) {
|
|
12
|
-
const allCards = cardsFromMsg(botMsg, true).reduce((acc, mc) => acc.concat([mc.text, mc.subtext, mc.content].filter(t => t)), [])
|
|
14
|
+
const allCards = cardsFromMsg(botMsg, true).reduce((acc, mc) => acc.concat([mc.text, mc.subtext, mc.content].filter(t => t).map(t => normalizeText(t, !!this.caps[SCRIPTING_NORMALIZE_TEXT]))), [])
|
|
13
15
|
if (!args || args.length === 0) {
|
|
14
16
|
return { allCards, cardsNotFound: [], cardsFound: allCards }
|
|
15
17
|
}
|
|
16
18
|
const cardsNotFound = []
|
|
17
19
|
const cardsFound = []
|
|
18
20
|
for (let i = 0; i < (args || []).length; i++) {
|
|
19
|
-
if (allCards.findIndex(c => this.context.Match(c, args[i])) < 0) {
|
|
21
|
+
if (allCards.findIndex(c => this.context.Match(c, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT]))) < 0) {
|
|
20
22
|
cardsNotFound.push(args[i])
|
|
21
23
|
} else {
|
|
22
24
|
cardsFound.push(args[i])
|
|
@@ -308,5 +308,18 @@ describe('compiler.compilertxt', function () {
|
|
|
308
308
|
assert.equal(convo.conversation[0].asserters.length, 1)
|
|
309
309
|
assert.deepEqual(convo.conversation[0].asserters[0], { name: 'BUTTONS', args: ['Test1', 'Test2'], not: true, optional: false })
|
|
310
310
|
})
|
|
311
|
+
it('should allow escape pipe for args', async function () {
|
|
312
|
+
const scriptBuffer = fs.readFileSync(path.resolve(__dirname, CONVOS_DIR, 'convos_args_escape.convo.txt'))
|
|
313
|
+
const context = buildContextWithPause()
|
|
314
|
+
const caps = {
|
|
315
|
+
}
|
|
316
|
+
const compiler = new Compiler(context, Object.assign({}, DefaultCapabilities, caps))
|
|
317
|
+
|
|
318
|
+
compiler.Compile(scriptBuffer, 'SCRIPTING_TYPE_CONVO')
|
|
319
|
+
const convo = context.convos[0]
|
|
320
|
+
assert.equal(convo.conversation.length, 1)
|
|
321
|
+
assert.equal(convo.conversation[0].asserters.length, 1)
|
|
322
|
+
assert.deepEqual(convo.conversation[0].asserters[0], { name: 'BUTTONS', args: ['Test|1', 'Test|2'], not: false, optional: false })
|
|
323
|
+
})
|
|
311
324
|
})
|
|
312
325
|
})
|
|
@@ -47,7 +47,8 @@ describe('convo.transcript', function () {
|
|
|
47
47
|
[Capabilities.PROJECTNAME]: 'convo.transcript',
|
|
48
48
|
[Capabilities.CONTAINERMODE]: echoConnector,
|
|
49
49
|
[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]: true,
|
|
50
|
-
[Capabilities.SCRIPTING_ENABLE_SKIP_ASSERT_ERRORS]: true
|
|
50
|
+
[Capabilities.SCRIPTING_ENABLE_SKIP_ASSERT_ERRORS]: true,
|
|
51
|
+
[Capabilities.WAITFORBOTTIMEOUT]: 500
|
|
51
52
|
}
|
|
52
53
|
this.driverSkipAssertErrors = new BotDriver(myCapsSkipAssertErrors)
|
|
53
54
|
this.compilerSkipAssertErrors = this.driverSkipAssertErrors.BuildCompiler()
|
|
@@ -381,4 +382,20 @@ describe('convo.transcript', function () {
|
|
|
381
382
|
assert.isNull(err.transcript.steps[5].err)
|
|
382
383
|
}
|
|
383
384
|
})
|
|
385
|
+
it('should continue on failing assertion with timeout', async function () {
|
|
386
|
+
this.compilerSkipAssertErrors.ReadScript(path.resolve(__dirname, 'convos'), 'continuefailing_timeout.convo.txt')
|
|
387
|
+
assert.equal(this.compilerSkipAssertErrors.convos.length, 1)
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
await this.compilerSkipAssertErrors.convos[0].Run(this.containerSkipAssertErrors)
|
|
391
|
+
assert.fail('expected error')
|
|
392
|
+
} catch (err) {
|
|
393
|
+
assert.isDefined(err.cause)
|
|
394
|
+
assert.equal(err.cause.message, 'continuefailing/Line 12: Bot response (on Line 9: #me - Hello again!) "Hello again!" expected to match "Hello again FAILING!",\ncontinuefailing/Line 15: error waiting for bot - Bot did not respond within 500ms')
|
|
395
|
+
assert.equal(err.cause.context.errors.length, 2)
|
|
396
|
+
|
|
397
|
+
assert.isDefined(err.transcript)
|
|
398
|
+
assert.equal(err.transcript.steps.length, 5)
|
|
399
|
+
}
|
|
400
|
+
})
|
|
384
401
|
})
|
|
@@ -263,3 +263,50 @@ describe('scripting.asserters.buttonsCountAsserter', function () {
|
|
|
263
263
|
})
|
|
264
264
|
})
|
|
265
265
|
})
|
|
266
|
+
|
|
267
|
+
describe('scripting.asserters.buttonsNormalizeAsserter', function () {
|
|
268
|
+
beforeEach(async function () {
|
|
269
|
+
this.cardsAsserter = new ButtonsAsserter({
|
|
270
|
+
Match: (botresponse, utterance) => botresponse.toLowerCase().indexOf(utterance.toLowerCase()) >= 0
|
|
271
|
+
}, { SCRIPTING_NORMALIZE_TEXT: true })
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should succeed with normalized text', async function () {
|
|
275
|
+
await this.cardsAsserter.assertConvoStep({
|
|
276
|
+
convoStep: { stepTag: 'test' },
|
|
277
|
+
args: ['Test Html header test html text'],
|
|
278
|
+
botMsg: {
|
|
279
|
+
buttons: [
|
|
280
|
+
{
|
|
281
|
+
text: '<html><h1>test html header</h1><p>test html text</p>'
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should fail with normalized text', async function () {
|
|
289
|
+
try {
|
|
290
|
+
await this.cardsAsserter.assertConvoStep({
|
|
291
|
+
convoStep: { stepTag: 'test' },
|
|
292
|
+
args: ['Test Html header1 test html text'],
|
|
293
|
+
botMsg: {
|
|
294
|
+
buttons: [
|
|
295
|
+
{
|
|
296
|
+
text: '<html><h1>test html header</h1><p>test html text</p>'
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
assert.fail('should have failed')
|
|
302
|
+
} catch (err) {
|
|
303
|
+
assert.isTrue(err.message.indexOf('Expected button(s) with text "Test Html header1 test html text"') > 0)
|
|
304
|
+
assert.isNotNull(err.context)
|
|
305
|
+
assert.isNotNull(err.context.cause)
|
|
306
|
+
assert.isArray(err.context.cause.expected)
|
|
307
|
+
assert.deepEqual(err.context.cause.expected, ['Test Html header1 test html text'])
|
|
308
|
+
assert.deepEqual(err.context.cause.actual, ['test html header test html text'])
|
|
309
|
+
assert.deepEqual(err.context.cause.diff, ['Test Html header1 test html text'])
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
})
|
|
@@ -243,3 +243,42 @@ describe('scripting.asserters.cardsCountAsserter', function () {
|
|
|
243
243
|
})
|
|
244
244
|
})
|
|
245
245
|
})
|
|
246
|
+
|
|
247
|
+
describe('scripting.asserters.cardsNormalizeAsserter', function () {
|
|
248
|
+
beforeEach(async function () {
|
|
249
|
+
this.cardsAsserter = new CardsAsserter({
|
|
250
|
+
Match: (botresponse, utterance) => botresponse.toLowerCase().indexOf(utterance.toLowerCase()) >= 0
|
|
251
|
+
}, { SCRIPTING_NORMALIZE_TEXT: true })
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('should succeed with normalized text', async function () {
|
|
255
|
+
await this.cardsAsserter.assertConvoStep({
|
|
256
|
+
convoStep: { stepTag: 'test' },
|
|
257
|
+
args: ['Test Html header test html text'],
|
|
258
|
+
botMsg: {
|
|
259
|
+
cards: [{ text: '<html><h1>test html header</h1><p>test html text</p>' }]
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should fail with normalized text', async function () {
|
|
265
|
+
try {
|
|
266
|
+
await this.cardsAsserter.assertConvoStep({
|
|
267
|
+
convoStep: { stepTag: 'test' },
|
|
268
|
+
args: ['Test Html header1 test html text'],
|
|
269
|
+
botMsg: {
|
|
270
|
+
cards: [{ text: '<html><h1>test html header</h1><p>test html text</p>' }]
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
assert.fail('should have failed')
|
|
274
|
+
} catch (err) {
|
|
275
|
+
assert.isTrue(err.message.indexOf('Expected card(s) with text "Test Html header1 test html text"') > 0)
|
|
276
|
+
assert.isNotNull(err.context)
|
|
277
|
+
assert.isNotNull(err.context.cause)
|
|
278
|
+
assert.isArray(err.context.cause.expected)
|
|
279
|
+
assert.deepEqual(err.context.cause.expected, ['Test Html header1 test html text'])
|
|
280
|
+
assert.deepEqual(err.context.cause.actual, ['test html header test html text'])
|
|
281
|
+
assert.deepEqual(err.context.cause.diff, ['Test Html header1 test html text'])
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
})
|
|
@@ -634,6 +634,30 @@ CARDS text of card2
|
|
|
634
634
|
#me
|
|
635
635
|
some text
|
|
636
636
|
CUSTOMINPUT arg1|arg2
|
|
637
|
+
`
|
|
638
|
+
)
|
|
639
|
+
})
|
|
640
|
+
it('should escape pipe in args', async function () {
|
|
641
|
+
const scriptingProvider = new ScriptingProvider(DefaultCapabilities)
|
|
642
|
+
await scriptingProvider.Build()
|
|
643
|
+
|
|
644
|
+
const convo = {
|
|
645
|
+
header: {
|
|
646
|
+
name: 'test convo'
|
|
647
|
+
},
|
|
648
|
+
conversation: [
|
|
649
|
+
{
|
|
650
|
+
sender: 'me',
|
|
651
|
+
userInputs: [{ name: 'CUSTOMINPUT', args: ['arg|1', 'arg|2'] }]
|
|
652
|
+
}
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const script = scriptingProvider.Decompile([convo], 'SCRIPTING_FORMAT_TXT')
|
|
657
|
+
assert.equal(script, `test convo
|
|
658
|
+
|
|
659
|
+
#me
|
|
660
|
+
CUSTOMINPUT arg\\|1|arg\\|2
|
|
637
661
|
`
|
|
638
662
|
)
|
|
639
663
|
})
|