@nyaruka/temba-components 0.129.8 → 0.129.9
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/CHANGELOG.md +27 -3
- package/demo/data/flows/sample-flow.json +186 -96
- package/dist/temba-components.js +414 -351
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/excellent/helpers.js +2 -2
- package/out-tsc/src/excellent/helpers.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +25 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +11 -1
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +133 -290
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
- package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
- package/out-tsc/src/flow/actions/call_llm.js +56 -3
- package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
- package/out-tsc/src/flow/actions/open_ticket.js +65 -3
- package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +75 -0
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/config.js +4 -0
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
- package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
- package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js +0 -65
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +18 -61
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +305 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -0
- package/out-tsc/src/form/FormField.js +3 -3
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/TextInput.js +1 -1
- package/out-tsc/src/form/TextInput.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +48 -20
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +39 -13
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/markdown.js +13 -11
- package/out-tsc/src/markdown.js.map +1 -1
- package/out-tsc/test/ActionHelper.js +2 -0
- package/out-tsc/test/ActionHelper.js.map +1 -1
- package/out-tsc/test/NodeHelper.js +148 -0
- package/out-tsc/test/NodeHelper.js.map +1 -0
- package/out-tsc/test/actions/call_llm.test.js +103 -0
- package/out-tsc/test/actions/call_llm.test.js.map +1 -0
- package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
- package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
- package/out-tsc/test/nodes/split_by_random.test.js +150 -0
- package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
- package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
- package/out-tsc/test/temba-add-input-labels.test.js +70 -0
- package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
- package/out-tsc/test/temba-field-renderer.test.js +296 -0
- package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
- package/out-tsc/test/temba-markdown.test.js +1 -1
- package/out-tsc/test/temba-markdown.test.js.map +1 -1
- package/out-tsc/test/temba-node-editor.test.js +400 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +6 -3
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +1 -1
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/send_msg.png +0 -0
- package/screenshots/truth/editor/set_contact_language.png +0 -0
- package/screenshots/truth/editor/set_contact_name.png +0 -0
- package/screenshots/truth/editor/set_run_result.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
- package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
- package/screenshots/truth/field-renderer/context-comparison.png +0 -0
- package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
- package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
- package/screenshots/truth/field-renderer/select-multi.png +0 -0
- package/screenshots/truth/field-renderer/select-no-label.png +0 -0
- package/screenshots/truth/field-renderer/select-with-label.png +0 -0
- package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
- package/screenshots/truth/field-renderer/text-no-label.png +0 -0
- package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
- package/screenshots/truth/field-renderer/text-with-label.png +0 -0
- package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
- package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/screenshots/truth/omnibox/selected.png +0 -0
- package/screenshots/truth/select/functions.png +0 -0
- package/screenshots/truth/select/multi-with-endpoint.png +0 -0
- package/screenshots/truth/select/search-enabled.png +0 -0
- package/src/events.ts +8 -1
- package/src/excellent/helpers.ts +2 -2
- package/src/flow/CanvasNode.ts +22 -1
- package/src/flow/Editor.ts +12 -1
- package/src/flow/NodeEditor.ts +186 -374
- package/src/flow/actions/add_input_labels.ts +45 -0
- package/src/flow/actions/call_llm.ts +57 -3
- package/src/flow/actions/call_webhook.ts +1 -1
- package/src/flow/actions/open_ticket.ts +74 -3
- package/src/flow/actions/set_run_result.ts +83 -0
- package/src/flow/config.ts +4 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
- package/src/flow/nodes/split_by_ticket.ts +19 -0
- package/src/flow/nodes/wait_for_response.ts +28 -1
- package/src/flow/types.ts +26 -127
- package/src/form/ArrayEditor.ts +34 -82
- package/src/form/FieldRenderer.ts +465 -0
- package/src/form/FormField.ts +3 -3
- package/src/form/TextInput.ts +1 -1
- package/src/form/select/Select.ts +51 -20
- package/src/live/ContactChat.ts +39 -15
- package/src/markdown.ts +19 -11
- package/src/store/flow-definition.d.ts +5 -2
- package/static/api/labels.json +31 -0
- package/static/api/topics.json +24 -9
- package/static/api/users.json +35 -16
- package/static/css/temba-components.css +3 -3
- package/stress-test.js +18 -13
- package/test/ActionHelper.ts +2 -0
- package/test/NodeHelper.ts +184 -0
- package/test/actions/call_llm.test.ts +137 -0
- package/test/nodes/README.md +78 -0
- package/test/nodes/split_by_llm_categorize.test.ts +698 -0
- package/test/nodes/split_by_random.test.ts +177 -0
- package/test/nodes/wait_for_digits.test.ts +176 -0
- package/test/nodes/wait_for_response.test.ts +206 -0
- package/test/temba-add-input-labels.test.ts +87 -0
- package/test/temba-field-renderer.test.ts +482 -0
- package/test/temba-markdown.test.ts +1 -1
- package/test/temba-node-editor.test.ts +496 -0
- package/test/temba-select.test.ts +6 -6
- package/test/temba-webchat.test.ts +1 -1
- package/test-assets/select/llms.json +18 -0
- package/web-dev-mock.mjs +96 -6
- package/web-dev-server.config.mjs +29 -7
- package/test/temba-flow-editor.test.ts.backup +0 -563
- package/test/temba-utils-index.test.ts.backup +0 -1737
package/src/live/ContactChat.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { ContactStoreElement } from './ContactStoreElement';
|
|
|
20
20
|
import { Compose, ComposeValue } from '../form/Compose';
|
|
21
21
|
import {
|
|
22
22
|
AirtimeTransferredEvent,
|
|
23
|
+
CallEvent,
|
|
23
24
|
ChannelEvent,
|
|
24
25
|
ContactEvent,
|
|
25
26
|
ContactGroupsEvent,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
FlowEvent,
|
|
29
30
|
MsgEvent,
|
|
30
31
|
NameChangedEvent,
|
|
31
|
-
|
|
32
|
+
OptInEvent,
|
|
32
33
|
RunEvent,
|
|
33
34
|
TicketEvent,
|
|
34
35
|
UpdateFieldEvent,
|
|
@@ -54,6 +55,8 @@ export enum Events {
|
|
|
54
55
|
MESSAGE_RECEIVED = 'msg_received',
|
|
55
56
|
BROADCAST_CREATED = 'broadcast_created',
|
|
56
57
|
IVR_CREATED = 'ivr_created',
|
|
58
|
+
CALL_CREATED = 'call_created',
|
|
59
|
+
CALL_RECEIVED = 'call_received',
|
|
57
60
|
CONTACT_FIELD_CHANGED = 'contact_field_changed',
|
|
58
61
|
CONTACT_GROUPS_CHANGED = 'contact_groups_changed',
|
|
59
62
|
CONTACT_NAME_CHANGED = 'contact_name_changed',
|
|
@@ -61,7 +64,6 @@ export enum Events {
|
|
|
61
64
|
CHANNEL_EVENT = 'channel_event',
|
|
62
65
|
CONTACT_LANGUAGE_CHANGED = 'contact_language_changed',
|
|
63
66
|
AIRTIME_TRANSFERRED = 'airtime_transferred',
|
|
64
|
-
CALL_STARTED = 'call_started',
|
|
65
67
|
NOTE_CREATED = 'note_created',
|
|
66
68
|
TICKET_ASSIGNED = 'ticket_assigned',
|
|
67
69
|
TICKET_NOTE_ADDED = 'ticket_note_added',
|
|
@@ -69,11 +71,14 @@ export enum Events {
|
|
|
69
71
|
TICKET_OPENED = 'ticket_opened',
|
|
70
72
|
TICKET_REOPENED = 'ticket_reopened',
|
|
71
73
|
TICKET_TOPIC_CHANGED = 'ticket_topic_changed',
|
|
74
|
+
OPTIN_STARTED = 'optin_started',
|
|
75
|
+
OPTIN_STOPPED = 'optin_stopped',
|
|
72
76
|
OPTIN_REQUESTED = 'optin_requested',
|
|
73
77
|
RUN_STARTED = 'run_started',
|
|
74
78
|
RUN_ENDED = 'run_ended',
|
|
75
79
|
|
|
76
80
|
// deprecated
|
|
81
|
+
CALL_STARTED = 'call_started',
|
|
77
82
|
FLOW_ENTERED = 'flow_entered',
|
|
78
83
|
FLOW_EXITED = 'flow_exited'
|
|
79
84
|
}
|
|
@@ -108,13 +113,13 @@ const renderChannelEvent = (event: ChannelEvent): string => {
|
|
|
108
113
|
} else if (event.event.type === 'stop_contact') {
|
|
109
114
|
return 'Stopped';
|
|
110
115
|
} else if (event.event.type === 'mt_call') {
|
|
111
|
-
return 'Outgoing Phone Call';
|
|
116
|
+
return 'Outgoing Phone Call'; // deprecated
|
|
112
117
|
} else if (event.event.type == 'mo_call') {
|
|
113
|
-
return 'Incoming Phone call';
|
|
118
|
+
return 'Incoming Phone call'; // deprecated
|
|
114
119
|
} else if (event.event.type == 'optin') {
|
|
115
|
-
return `Opted in to **${event.event.optin?.name}**`;
|
|
120
|
+
return `Opted in to **${event.event.optin?.name}**`; // deprecated
|
|
116
121
|
} else if (event.event.type == 'optout') {
|
|
117
|
-
return `Opted out of **${event.event.optin?.name}**`;
|
|
122
|
+
return `Opted out of **${event.event.optin?.name}**`; // deprecated
|
|
118
123
|
}
|
|
119
124
|
};
|
|
120
125
|
|
|
@@ -214,18 +219,28 @@ export const renderAirtimeTransferredEvent = (
|
|
|
214
219
|
return `Transferred **${event.amount}** ${event.currency} of airtime`;
|
|
215
220
|
};
|
|
216
221
|
|
|
217
|
-
export const renderCallStartedEvent = (): string => {
|
|
218
|
-
return `Call Started`;
|
|
219
|
-
};
|
|
220
|
-
|
|
221
222
|
export const renderContactLanguageChangedEvent = (
|
|
222
223
|
event: ContactLanguageChangedEvent
|
|
223
224
|
): string => {
|
|
224
225
|
return `Language updated to **${event.language}**`;
|
|
225
226
|
};
|
|
226
227
|
|
|
227
|
-
export const
|
|
228
|
-
|
|
228
|
+
export const renderCallEvent = (event: CallEvent): string => {
|
|
229
|
+
if (event.type === Events.CALL_CREATED) {
|
|
230
|
+
return `Call Started`;
|
|
231
|
+
} else if (event.type === Events.CALL_RECEIVED) {
|
|
232
|
+
return `Call Answered`;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const renderOptInEvent = (event: OptInEvent): string => {
|
|
237
|
+
if (event.type === Events.OPTIN_REQUESTED) {
|
|
238
|
+
return `Requested opt-in for ${event.optin.name}`;
|
|
239
|
+
} else if (event.type === Events.OPTIN_STARTED) {
|
|
240
|
+
return `Opted in to **${event.optin.name}**`;
|
|
241
|
+
} else if (event.type === Events.OPTIN_STOPPED) {
|
|
242
|
+
return `Opted out of **${event.optin.name}**`;
|
|
243
|
+
}
|
|
229
244
|
};
|
|
230
245
|
|
|
231
246
|
export class ContactChat extends ContactStoreElement {
|
|
@@ -694,10 +709,17 @@ export class ContactChat extends ContactStoreElement {
|
|
|
694
709
|
text: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent)
|
|
695
710
|
};
|
|
696
711
|
break;
|
|
697
|
-
case Events.
|
|
712
|
+
case Events.CALL_CREATED:
|
|
713
|
+
case Events.CALL_RECEIVED:
|
|
714
|
+
message = {
|
|
715
|
+
type: MessageType.Inline,
|
|
716
|
+
text: renderCallEvent(event as CallEvent)
|
|
717
|
+
};
|
|
718
|
+
break;
|
|
719
|
+
case Events.CALL_STARTED: // deprecated
|
|
698
720
|
message = {
|
|
699
721
|
type: MessageType.Inline,
|
|
700
|
-
text:
|
|
722
|
+
text: `Started Call`
|
|
701
723
|
};
|
|
702
724
|
break;
|
|
703
725
|
case Events.CHANNEL_EVENT:
|
|
@@ -715,9 +737,11 @@ export class ContactChat extends ContactStoreElement {
|
|
|
715
737
|
};
|
|
716
738
|
break;
|
|
717
739
|
case Events.OPTIN_REQUESTED:
|
|
740
|
+
case Events.OPTIN_STARTED:
|
|
741
|
+
case Events.OPTIN_STOPPED:
|
|
718
742
|
message = {
|
|
719
743
|
type: MessageType.Inline,
|
|
720
|
-
text:
|
|
744
|
+
text: renderOptInEvent(event as OptInEvent)
|
|
721
745
|
};
|
|
722
746
|
break;
|
|
723
747
|
}
|
package/src/markdown.ts
CHANGED
|
@@ -11,31 +11,39 @@ import { Remarkable } from 'remarkable';
|
|
|
11
11
|
|
|
12
12
|
export const markdown = new Remarkable();
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// State stored in class field
|
|
17
|
-
// value: string | undefined;
|
|
14
|
+
// Base class for markdown rendering directives
|
|
15
|
+
abstract class BaseMarkdownDirective extends Directive {
|
|
18
16
|
constructor(partInfo: PartInfo) {
|
|
19
17
|
super(partInfo);
|
|
20
18
|
// When necessary, validate part in constructor using `part.type`
|
|
21
19
|
if (partInfo.type !== PartType.CHILD) {
|
|
22
|
-
throw new Error('
|
|
20
|
+
throw new Error('markdown directives only support child expressions');
|
|
23
21
|
}
|
|
24
22
|
}
|
|
23
|
+
|
|
25
24
|
// Optional: override update to perform any direct DOM manipulation
|
|
26
|
-
// DirectiveParameters<this>
|
|
27
25
|
update(part: Part, [initialValue]: any) {
|
|
28
26
|
/* Any imperative updates to DOM/parts would go here */
|
|
29
27
|
return this.render(initialValue);
|
|
30
28
|
}
|
|
31
|
-
|
|
29
|
+
|
|
30
|
+
// Abstract method to be implemented by subclasses
|
|
31
|
+
abstract render(initialValue: string): any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Class-based directive for block markdown rendering
|
|
35
|
+
export class RenderMarkdown extends BaseMarkdownDirective {
|
|
32
36
|
render(initialValue: string) {
|
|
33
|
-
// Previous state available on class field
|
|
34
|
-
// if (this.value === undefined) {
|
|
35
|
-
// this.value = initialValue;
|
|
36
|
-
//}
|
|
37
37
|
return html`${unsafeHTML(markdown.render(initialValue))}`;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Class-based directive for inline markdown rendering
|
|
42
|
+
export class RenderMarkdownInline extends BaseMarkdownDirective {
|
|
43
|
+
render(initialValue: string) {
|
|
44
|
+
return html`${unsafeHTML(markdown.renderInline(initialValue))}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
export const renderMarkdown = directive(RenderMarkdown);
|
|
49
|
+
export const renderMarkdownInline = directive(RenderMarkdownInline);
|
|
@@ -44,6 +44,7 @@ export type ActionType =
|
|
|
44
44
|
| 'split_by_subflow'
|
|
45
45
|
| 'split_by_webhook'
|
|
46
46
|
| 'split_by_llm'
|
|
47
|
+
| 'split_by_llm_categorize'
|
|
47
48
|
| 'wait_for_response'
|
|
48
49
|
| 'wait_for_menu'
|
|
49
50
|
| 'wait_for_dial'
|
|
@@ -173,12 +174,14 @@ export interface CallResthook extends Action {
|
|
|
173
174
|
export interface CallLLM extends Action {
|
|
174
175
|
llm: NamedObject;
|
|
175
176
|
instructions: string;
|
|
177
|
+
input: string;
|
|
176
178
|
result_name: string;
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
export interface OpenTicket extends Action {
|
|
180
|
-
subject
|
|
181
|
-
body
|
|
182
|
+
subject?: string;
|
|
183
|
+
body?: string;
|
|
184
|
+
note?: string;
|
|
182
185
|
assignee?: NamedObject;
|
|
183
186
|
topic?: NamedObject;
|
|
184
187
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"next": null,
|
|
3
|
+
"previous": null,
|
|
4
|
+
"results": [
|
|
5
|
+
{
|
|
6
|
+
"uuid": "61cae99b-56e1-4f3e-a2b9-07fb5cf2be9e",
|
|
7
|
+
"name": "Important",
|
|
8
|
+
"count": 234
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"uuid": "f80ed7b6-5e6a-49eb-94b6-4631a9677cd8",
|
|
12
|
+
"name": "Spam",
|
|
13
|
+
"count": 0
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"uuid": "76e7a094-22ea-441d-91c4-283d8168e8e3",
|
|
17
|
+
"name": "Follow Up",
|
|
18
|
+
"count": 128
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"uuid": "66e7a094-22ea-441d-91c4-283d8168e8e4",
|
|
22
|
+
"name": "Customer Service",
|
|
23
|
+
"count": 89
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"uuid": "a3f2990b-4096-452e-a586-dc06a9434dde",
|
|
27
|
+
"name": "Feedback",
|
|
28
|
+
"count": 42
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
package/static/api/topics.json
CHANGED
|
@@ -3,19 +3,34 @@
|
|
|
3
3
|
"previous": null,
|
|
4
4
|
"results": [
|
|
5
5
|
{
|
|
6
|
-
"uuid": "
|
|
7
|
-
"name": "General
|
|
8
|
-
"
|
|
6
|
+
"uuid": "1b1cb507-e079-4b30-818c-1898edcbd178",
|
|
7
|
+
"name": "General",
|
|
8
|
+
"counts": {
|
|
9
|
+
"open": 2,
|
|
10
|
+
"closed": 12
|
|
11
|
+
},
|
|
12
|
+
"system": true,
|
|
13
|
+
"created_on": "2021-08-25T22:50:51.381947Z"
|
|
9
14
|
},
|
|
10
15
|
{
|
|
11
|
-
"uuid": "
|
|
12
|
-
"name": "Technical
|
|
13
|
-
"
|
|
16
|
+
"uuid": "bf4b568d-97b8-4d20-aed5-ad8150270af8",
|
|
17
|
+
"name": "Technical Support",
|
|
18
|
+
"counts": {
|
|
19
|
+
"open": 5,
|
|
20
|
+
"closed": 23
|
|
21
|
+
},
|
|
22
|
+
"system": false,
|
|
23
|
+
"created_on": "2021-08-25T22:51:15.123456Z"
|
|
14
24
|
},
|
|
15
25
|
{
|
|
16
|
-
"uuid": "
|
|
17
|
-
"name": "Billing
|
|
18
|
-
"
|
|
26
|
+
"uuid": "a3f7e9d2-1c8b-4e5f-9a6b-7d4c2e8f1a3b",
|
|
27
|
+
"name": "Billing",
|
|
28
|
+
"counts": {
|
|
29
|
+
"open": 1,
|
|
30
|
+
"closed": 8
|
|
31
|
+
},
|
|
32
|
+
"system": false,
|
|
33
|
+
"created_on": "2021-08-25T22:52:30.789012Z"
|
|
19
34
|
}
|
|
20
35
|
]
|
|
21
36
|
}
|
package/static/api/users.json
CHANGED
|
@@ -3,24 +3,43 @@
|
|
|
3
3
|
"previous": null,
|
|
4
4
|
"results": [
|
|
5
5
|
{
|
|
6
|
-
"uuid": "
|
|
7
|
-
"email": "
|
|
8
|
-
"first_name": "
|
|
9
|
-
"last_name": "
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
6
|
+
"uuid": "c0f5b431-35e9-429c-9d57-5fee2fac46a3",
|
|
7
|
+
"email": "eric+marion+berry@textit.com",
|
|
8
|
+
"first_name": "Marion",
|
|
9
|
+
"last_name": "Berry",
|
|
10
|
+
"name": "Marion Berry",
|
|
11
|
+
"role": "agent",
|
|
12
|
+
"team": {
|
|
13
|
+
"uuid": "15236c1e-9375-4f84-bb48-ec64283d1eb9",
|
|
14
|
+
"name": "All Topics"
|
|
15
|
+
},
|
|
16
|
+
"created_on": "2023-04-05T21:11:31.909765Z",
|
|
17
|
+
"avatar": null
|
|
14
18
|
},
|
|
15
19
|
{
|
|
16
|
-
"uuid": "
|
|
17
|
-
"email": "
|
|
18
|
-
"first_name": "
|
|
19
|
-
"last_name": "
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
20
|
+
"uuid": "ae79dd5b-8c34-4602-a2c1-1e4db2419f0f",
|
|
21
|
+
"email": "eric@textit.com",
|
|
22
|
+
"first_name": "Eric",
|
|
23
|
+
"last_name": "Newcomer",
|
|
24
|
+
"name": "Eric Newcomer",
|
|
25
|
+
"role": "administrator",
|
|
26
|
+
"team": null,
|
|
27
|
+
"created_on": "2013-02-26T21:19:44Z",
|
|
28
|
+
"avatar": "https://dl-textit.s3.amazonaws.com/avatars/4/b6d756224c61435bb36b57ae03b83359.jpg"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"uuid": "f8e2a1b5-9c7d-4e6f-8a3b-1c5e9f2d4a6b",
|
|
32
|
+
"email": "sarah.smith@textit.com",
|
|
33
|
+
"first_name": "Sarah",
|
|
34
|
+
"last_name": "Smith",
|
|
35
|
+
"name": "Sarah Smith",
|
|
36
|
+
"role": "agent",
|
|
37
|
+
"team": {
|
|
38
|
+
"uuid": "15236c1e-9375-4f84-bb48-ec64283d1eb9",
|
|
39
|
+
"name": "All Topics"
|
|
40
|
+
},
|
|
41
|
+
"created_on": "2023-06-15T14:22:18.456789Z",
|
|
42
|
+
"avatar": null
|
|
24
43
|
}
|
|
25
44
|
]
|
|
26
45
|
}
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
--color-text-light: rgba(255, 255, 255, 1);
|
|
62
62
|
--color-text-dark: rgba(0, 0, 0, 0.8);
|
|
63
63
|
--color-text-dark-secondary: rgba(0, 0, 0, 0.25);
|
|
64
|
-
--color-text-help:
|
|
64
|
+
--color-text-help: rgb(120, 120, 120);
|
|
65
65
|
--color-tertiary: rgb(var(--tertiary-rgb));
|
|
66
66
|
|
|
67
67
|
--help-text-size: 0.85em;
|
|
@@ -125,8 +125,8 @@
|
|
|
125
125
|
--header-bg: var(--color-primary-dark);
|
|
126
126
|
--header-text: var(--color-text-light);
|
|
127
127
|
|
|
128
|
-
--temba-textinput-padding: 9px;
|
|
129
|
-
--temba-textinput-font-size:
|
|
128
|
+
--temba-textinput-padding: 9px 14px;
|
|
129
|
+
--temba-textinput-font-size: 14px;
|
|
130
130
|
|
|
131
131
|
--options-block-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
|
132
132
|
--options-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
package/stress-test.js
CHANGED
|
@@ -11,7 +11,7 @@ let maxRuns = 10;
|
|
|
11
11
|
// Parse arguments
|
|
12
12
|
for (let i = 0; i < args.length; i++) {
|
|
13
13
|
const arg = args[i];
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
if (arg.startsWith('--runs=')) {
|
|
16
16
|
maxRuns = parseInt(arg.split('=')[1]);
|
|
17
17
|
if (isNaN(maxRuns) || maxRuns <= 0) {
|
|
@@ -28,12 +28,16 @@ for (let i = 0; i < args.length; i++) {
|
|
|
28
28
|
// Validate test file
|
|
29
29
|
if (!testFile) {
|
|
30
30
|
console.error('❌ Usage: yarn stress-test <test-file> [--runs=N]');
|
|
31
|
-
console.error(
|
|
31
|
+
console.error(
|
|
32
|
+
' Example: yarn stress-test test/temba-webchat.test.ts --runs=100'
|
|
33
|
+
);
|
|
32
34
|
process.exit(1);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
if (!testFile.startsWith('test/') || !testFile.endsWith('.test.ts')) {
|
|
36
|
-
console.error(
|
|
38
|
+
console.error(
|
|
39
|
+
'❌ Test file must be in the test/ directory and end with .test.ts'
|
|
40
|
+
);
|
|
37
41
|
process.exit(1);
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -51,21 +55,21 @@ const startTime = performance.now();
|
|
|
51
55
|
try {
|
|
52
56
|
while (run <= maxRuns) {
|
|
53
57
|
const runStartTime = performance.now();
|
|
54
|
-
|
|
58
|
+
|
|
55
59
|
process.stdout.write(`Run ${run.toString().padStart(3)}/${maxRuns}: `);
|
|
56
|
-
|
|
60
|
+
|
|
57
61
|
try {
|
|
58
62
|
// Run the test with minimal output
|
|
59
|
-
const result = execSync(`yarn test ${testFile}`, {
|
|
63
|
+
const result = execSync(`yarn test ${testFile}`, {
|
|
60
64
|
encoding: 'utf-8',
|
|
61
65
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
62
66
|
});
|
|
63
|
-
|
|
67
|
+
|
|
64
68
|
const runEndTime = performance.now();
|
|
65
69
|
const runTime = runEndTime - runStartTime;
|
|
66
70
|
runTimes.push(runTime);
|
|
67
71
|
totalTime += runTime;
|
|
68
|
-
|
|
72
|
+
|
|
69
73
|
// Check if the test actually passed by looking for success indicators
|
|
70
74
|
if (result.includes('all tests passed') || result.includes('0 failed')) {
|
|
71
75
|
console.log(`✅ PASS (${(runTime / 1000).toFixed(2)}s)`);
|
|
@@ -75,11 +79,10 @@ try {
|
|
|
75
79
|
failures++;
|
|
76
80
|
break;
|
|
77
81
|
}
|
|
78
|
-
|
|
79
82
|
} catch (error) {
|
|
80
83
|
const runEndTime = performance.now();
|
|
81
84
|
const runTime = runEndTime - runStartTime;
|
|
82
|
-
|
|
85
|
+
|
|
83
86
|
console.log(`❌ FAIL (${(runTime / 1000).toFixed(2)}s)`);
|
|
84
87
|
console.log('');
|
|
85
88
|
console.log('💥 Test failed on run', run);
|
|
@@ -94,7 +97,7 @@ try {
|
|
|
94
97
|
failures++;
|
|
95
98
|
break;
|
|
96
99
|
}
|
|
97
|
-
|
|
100
|
+
|
|
98
101
|
run++;
|
|
99
102
|
}
|
|
100
103
|
} catch (error) {
|
|
@@ -112,14 +115,16 @@ console.log('==================');
|
|
|
112
115
|
console.log(`Test file: ${testFile}`);
|
|
113
116
|
console.log(`Completed runs: ${run - 1}/${maxRuns}`);
|
|
114
117
|
console.log(`Failures: ${failures}`);
|
|
115
|
-
console.log(
|
|
118
|
+
console.log(
|
|
119
|
+
`Success rate: ${(((run - 1 - failures) / (run - 1)) * 100).toFixed(1)}%`
|
|
120
|
+
);
|
|
116
121
|
console.log('');
|
|
117
122
|
|
|
118
123
|
if (runTimes.length > 0) {
|
|
119
124
|
const avgTime = runTimes.reduce((a, b) => a + b, 0) / runTimes.length;
|
|
120
125
|
const minTime = Math.min(...runTimes);
|
|
121
126
|
const maxTime = Math.max(...runTimes);
|
|
122
|
-
|
|
127
|
+
|
|
123
128
|
console.log('⏱️ Timing Statistics');
|
|
124
129
|
console.log('=====================');
|
|
125
130
|
console.log(`Total time: ${(totalTestTime / 1000).toFixed(2)}s`);
|
package/test/ActionHelper.ts
CHANGED
|
@@ -8,6 +8,8 @@ import '../temba-modules';
|
|
|
8
8
|
/**
|
|
9
9
|
* Generic action test framework
|
|
10
10
|
* Tests the complete action lifecycle: render → edit → save → validate
|
|
11
|
+
*
|
|
12
|
+
* For node configuration testing, see NodeHelper.ts
|
|
11
13
|
*/
|
|
12
14
|
export class ActionTest<T extends Action> {
|
|
13
15
|
constructor(private actionConfig: any, private actionName: string) {}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { fixture, expect } from '@open-wc/testing';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
import { Node } from '../src/store/flow-definition';
|
|
4
|
+
import { assertScreenshot, getClip } from './utils.test';
|
|
5
|
+
import { Editor } from '../src/flow/Editor';
|
|
6
|
+
import '../temba-modules';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generic node test framework
|
|
10
|
+
* Tests the complete node lifecycle: render → edit → save → validate
|
|
11
|
+
*
|
|
12
|
+
* This is the node configuration equivalent of ActionHelper.ts for action configurations.
|
|
13
|
+
* It provides uniform testing for all types of nodes: simple wait nodes, router-based
|
|
14
|
+
* split nodes, and complex form-configured nodes.
|
|
15
|
+
*/
|
|
16
|
+
export class NodeTest<T extends Node> {
|
|
17
|
+
constructor(private nodeConfig: any, private nodeName: string) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Renders a node in the flow editor and returns the flow node
|
|
21
|
+
*/
|
|
22
|
+
private async renderNode(node: T, nodeUI: any): Promise<HTMLElement> {
|
|
23
|
+
const mockDefinition = {
|
|
24
|
+
nodes: [node],
|
|
25
|
+
_ui: {
|
|
26
|
+
nodes: {
|
|
27
|
+
[node.uuid]: {
|
|
28
|
+
type: nodeUI.type,
|
|
29
|
+
position: { left: 50, top: 50 },
|
|
30
|
+
...nodeUI
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const editor = (await fixture(html`
|
|
37
|
+
<temba-flow-editor>
|
|
38
|
+
<div id="canvas"></div>
|
|
39
|
+
</temba-flow-editor>
|
|
40
|
+
`)) as Editor;
|
|
41
|
+
|
|
42
|
+
(editor as any).definition = mockDefinition;
|
|
43
|
+
(editor as any).canvasSize = { width: 400, height: 300 };
|
|
44
|
+
await editor.updateComplete;
|
|
45
|
+
|
|
46
|
+
const flowNode = editor.querySelector('temba-flow-node') as HTMLElement;
|
|
47
|
+
expect(flowNode).to.exist;
|
|
48
|
+
|
|
49
|
+
return flowNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Opens the node editor for a node and returns the editor element
|
|
54
|
+
*/
|
|
55
|
+
private async openNodeEditor(node: T, nodeUI: any): Promise<HTMLElement> {
|
|
56
|
+
const nodeEditor = (await fixture(html`
|
|
57
|
+
<temba-node-editor
|
|
58
|
+
.node=${node}
|
|
59
|
+
.nodeUI=${nodeUI}
|
|
60
|
+
.isOpen=${true}
|
|
61
|
+
></temba-node-editor>
|
|
62
|
+
`)) as HTMLElement;
|
|
63
|
+
|
|
64
|
+
await (nodeEditor as any).updateComplete;
|
|
65
|
+
|
|
66
|
+
// Wait for form data initialization if needed
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
|
+
await (nodeEditor as any).updateComplete;
|
|
69
|
+
|
|
70
|
+
expect(nodeEditor).to.exist;
|
|
71
|
+
|
|
72
|
+
return nodeEditor;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Takes a screenshot of the dialog container within a node editor
|
|
77
|
+
*/
|
|
78
|
+
private async assertDialogScreenshot(
|
|
79
|
+
el: HTMLElement,
|
|
80
|
+
screenshotName: string
|
|
81
|
+
) {
|
|
82
|
+
const dialog = el.shadowRoot
|
|
83
|
+
?.querySelector('temba-dialog')
|
|
84
|
+
?.shadowRoot?.querySelector('.dialog-container') as HTMLElement;
|
|
85
|
+
await assertScreenshot(screenshotName, getClip(dialog));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Complete test for a node configuration
|
|
90
|
+
* 1. Renders the node in a flow node (with screenshot)
|
|
91
|
+
* 2. Opens the node editor (with screenshot)
|
|
92
|
+
* 3. Simulates save and validates round-trip conversion
|
|
93
|
+
*/
|
|
94
|
+
async testNode(node: T, nodeUI: any, testName: string) {
|
|
95
|
+
it(`${testName}`, async () => {
|
|
96
|
+
// Step 1: Render node in flow node
|
|
97
|
+
const flowNode = await this.renderNode(node, nodeUI);
|
|
98
|
+
|
|
99
|
+
// For execute_actions nodes, check for .body, for router nodes check for .router or .categories
|
|
100
|
+
const hasContent =
|
|
101
|
+
flowNode.querySelector('.body') ||
|
|
102
|
+
flowNode.querySelector('.router') ||
|
|
103
|
+
flowNode.querySelector('.categories') ||
|
|
104
|
+
flowNode.querySelector('.action') ||
|
|
105
|
+
flowNode.textContent?.trim();
|
|
106
|
+
|
|
107
|
+
expect(hasContent).to.exist;
|
|
108
|
+
await assertScreenshot(
|
|
109
|
+
`nodes/${this.nodeName}/render/${testName}`,
|
|
110
|
+
getClip(flowNode)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Step 2: Open node editor
|
|
114
|
+
const nodeEditor = await this.openNodeEditor(node, nodeUI);
|
|
115
|
+
await this.assertDialogScreenshot(
|
|
116
|
+
nodeEditor,
|
|
117
|
+
`nodes/${this.nodeName}/editor/${testName}`
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Step 3: Test round-trip conversion (simulates save workflow)
|
|
121
|
+
if (this.nodeConfig.toFormData && this.nodeConfig.fromFormData) {
|
|
122
|
+
const formData = this.nodeConfig.toFormData(node);
|
|
123
|
+
const convertedNode = this.nodeConfig.fromFormData(formData, node) as T;
|
|
124
|
+
|
|
125
|
+
// Validate the round trip worked
|
|
126
|
+
expect(convertedNode.uuid).to.equal(node.uuid);
|
|
127
|
+
|
|
128
|
+
// Validate the converted node has expected structure
|
|
129
|
+
expect(convertedNode).to.have.property('actions');
|
|
130
|
+
expect(convertedNode).to.have.property('exits');
|
|
131
|
+
|
|
132
|
+
expect(convertedNode).to.deep.equal(node);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Run basic property tests
|
|
139
|
+
*/
|
|
140
|
+
testBasicProperties() {
|
|
141
|
+
it('has correct basic properties', () => {
|
|
142
|
+
expect(this.nodeConfig.type).to.be.a('string');
|
|
143
|
+
|
|
144
|
+
// Name is optional - only some node configs have it
|
|
145
|
+
if (this.nodeConfig.name) {
|
|
146
|
+
expect(this.nodeConfig.name).to.be.a('string');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Color is optional
|
|
150
|
+
if (this.nodeConfig.color) {
|
|
151
|
+
expect(this.nodeConfig.color).to.be.a('string');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// toFormData and fromFormData are optional - only needed for complex data transformations
|
|
155
|
+
if (this.nodeConfig.toFormData) {
|
|
156
|
+
expect(this.nodeConfig.toFormData).to.be.a('function');
|
|
157
|
+
}
|
|
158
|
+
if (this.nodeConfig.fromFormData) {
|
|
159
|
+
expect(this.nodeConfig.fromFormData).to.be.a('function');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Form configuration is optional
|
|
163
|
+
if (this.nodeConfig.form) {
|
|
164
|
+
expect(this.nodeConfig.form).to.be.an('object');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Layout is optional
|
|
168
|
+
if (this.nodeConfig.layout) {
|
|
169
|
+
expect(this.nodeConfig.layout).to.be.an('array');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Router config is optional
|
|
173
|
+
if (this.nodeConfig.router) {
|
|
174
|
+
expect(this.nodeConfig.router).to.be.an('object');
|
|
175
|
+
expect(this.nodeConfig.router.type).to.exist;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Render function is optional
|
|
179
|
+
if (this.nodeConfig.render) {
|
|
180
|
+
expect(this.nodeConfig.render).to.be.a('function');
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|