@nyaruka/temba-components 0.129.7 → 0.129.8
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/.devcontainer/Dockerfile +11 -4
- package/.devcontainer/devcontainer.json +3 -2
- package/.github/workflows/build.yml +4 -14
- package/CHANGELOG.md +8 -3
- package/demo/components/flow/example.html +1 -1
- package/demo/components/message-editor/example.html +125 -0
- package/demo/components/textinput/completion.html +1 -0
- package/demo/data/flows/food-order.json +12 -21
- package/demo/data/flows/sample-flow.json +42 -26
- package/dist/temba-components.js +506 -218
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +2 -1
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +245 -22
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js +26 -17
- package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +147 -6
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +111 -38
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/BaseListEditor.js +19 -4
- package/out-tsc/src/form/BaseListEditor.js.map +1 -1
- package/out-tsc/src/form/FormField.js +1 -1
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/KeyValueEditor.js +1 -1
- package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
- package/out-tsc/src/form/MediaPicker.js +13 -1
- package/out-tsc/src/form/MediaPicker.js.map +1 -1
- package/out-tsc/src/form/MessageEditor.js +422 -0
- package/out-tsc/src/form/MessageEditor.js.map +1 -0
- package/out-tsc/src/form/TextInput.js +12 -5
- package/out-tsc/src/form/TextInput.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +4 -4
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +27 -2
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-field-config.test.js +4 -2
- package/out-tsc/test/temba-field-config.test.js.map +1 -1
- package/out-tsc/test/temba-message-editor.test.js +194 -0
- package/out-tsc/test/temba-message-editor.test.js.map +1 -0
- package/out-tsc/test/temba-node-editor.test.js +71 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +1 -1
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-textinput.test.js +16 -0
- package/out-tsc/test/temba-textinput.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +4 -0
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +2 -8
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +7 -4
- 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/add_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.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/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
- package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/with-expressions.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/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/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
- package/screenshots/truth/message-editor/default.png +0 -0
- package/screenshots/truth/message-editor/drag-highlight.png +0 -0
- package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
- package/screenshots/truth/message-editor/with-completion.png +0 -0
- package/screenshots/truth/message-editor/with-properties.png +0 -0
- package/screenshots/truth/textinput/autogrow-initial.png +0 -0
- package/screenshots/truth/textinput/input-form.png +0 -0
- package/src/display/Thumbnail.ts +2 -1
- package/src/events.ts +5 -0
- package/src/flow/NodeEditor.ts +269 -23
- package/src/flow/actions/call_webhook.ts +28 -18
- package/src/flow/actions/send_msg.ts +170 -6
- package/src/flow/types.ts +21 -2
- package/src/form/ArrayEditor.ts +120 -42
- package/src/form/BaseListEditor.ts +22 -6
- package/src/form/FormField.ts +1 -1
- package/src/form/KeyValueEditor.ts +1 -1
- package/src/form/MediaPicker.ts +13 -1
- package/src/form/MessageEditor.ts +449 -0
- package/src/form/TextInput.ts +15 -7
- package/src/form/select/Select.ts +4 -4
- package/src/live/ContactChat.ts +30 -4
- package/static/css/temba-components.css +2 -0
- package/static/mr/docs/en-us/editor.json +2588 -0
- package/stress-test.js +138 -0
- package/temba-modules.ts +2 -0
- package/test/temba-field-config.test.ts +4 -2
- package/test/temba-message-editor.test.ts +300 -0
- package/test/temba-node-editor.test.ts +94 -0
- package/test/temba-select.test.ts +1 -1
- package/test/temba-textinput.test.ts +26 -0
- package/test/temba-webchat.test.ts +5 -0
- package/test/utils.test.ts +2 -13
- package/test-assets/contacts/history.json +19 -0
- package/test-assets/style.css +2 -0
- package/web-dev-mock.mjs +433 -0
- package/web-dev-server.config.mjs +51 -5
- package/web-test-runner.config.mjs +9 -4
package/stress-test.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { performance } from 'perf_hooks';
|
|
5
|
+
|
|
6
|
+
// Parse command line arguments
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
let testFile = '';
|
|
9
|
+
let maxRuns = 10;
|
|
10
|
+
|
|
11
|
+
// Parse arguments
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
|
|
15
|
+
if (arg.startsWith('--runs=')) {
|
|
16
|
+
maxRuns = parseInt(arg.split('=')[1]);
|
|
17
|
+
if (isNaN(maxRuns) || maxRuns <= 0) {
|
|
18
|
+
console.error('❌ Invalid runs value. Must be a positive integer.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
} else if (arg.startsWith('test/') && arg.endsWith('.test.ts')) {
|
|
22
|
+
testFile = arg;
|
|
23
|
+
} else if (!arg.startsWith('--')) {
|
|
24
|
+
testFile = arg;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Validate test file
|
|
29
|
+
if (!testFile) {
|
|
30
|
+
console.error('❌ Usage: yarn stress-test <test-file> [--runs=N]');
|
|
31
|
+
console.error(' Example: yarn stress-test test/temba-webchat.test.ts --runs=100');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!testFile.startsWith('test/') || !testFile.endsWith('.test.ts')) {
|
|
36
|
+
console.error('❌ Test file must be in the test/ directory and end with .test.ts');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`🧪 Stress testing: ${testFile}`);
|
|
41
|
+
console.log(`🔄 Running up to ${maxRuns} times (or until failure)`);
|
|
42
|
+
console.log('');
|
|
43
|
+
|
|
44
|
+
let run = 1;
|
|
45
|
+
let totalTime = 0;
|
|
46
|
+
const runTimes = [];
|
|
47
|
+
let failures = 0;
|
|
48
|
+
|
|
49
|
+
const startTime = performance.now();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
while (run <= maxRuns) {
|
|
53
|
+
const runStartTime = performance.now();
|
|
54
|
+
|
|
55
|
+
process.stdout.write(`Run ${run.toString().padStart(3)}/${maxRuns}: `);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Run the test with minimal output
|
|
59
|
+
const result = execSync(`yarn test ${testFile}`, {
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const runEndTime = performance.now();
|
|
65
|
+
const runTime = runEndTime - runStartTime;
|
|
66
|
+
runTimes.push(runTime);
|
|
67
|
+
totalTime += runTime;
|
|
68
|
+
|
|
69
|
+
// Check if the test actually passed by looking for success indicators
|
|
70
|
+
if (result.includes('all tests passed') || result.includes('0 failed')) {
|
|
71
|
+
console.log(`✅ PASS (${(runTime / 1000).toFixed(2)}s)`);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`❌ FAIL (unexpected output)`);
|
|
74
|
+
console.log('Output:', result);
|
|
75
|
+
failures++;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const runEndTime = performance.now();
|
|
81
|
+
const runTime = runEndTime - runStartTime;
|
|
82
|
+
|
|
83
|
+
console.log(`❌ FAIL (${(runTime / 1000).toFixed(2)}s)`);
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log('💥 Test failed on run', run);
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log('Error output:');
|
|
88
|
+
console.log(error.stdout || error.message);
|
|
89
|
+
if (error.stderr) {
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log('Error details:');
|
|
92
|
+
console.log(error.stderr);
|
|
93
|
+
}
|
|
94
|
+
failures++;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
run++;
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('💥 Unexpected error:', error.message);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const endTime = performance.now();
|
|
107
|
+
const totalTestTime = endTime - startTime;
|
|
108
|
+
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log('📊 Results Summary');
|
|
111
|
+
console.log('==================');
|
|
112
|
+
console.log(`Test file: ${testFile}`);
|
|
113
|
+
console.log(`Completed runs: ${run - 1}/${maxRuns}`);
|
|
114
|
+
console.log(`Failures: ${failures}`);
|
|
115
|
+
console.log(`Success rate: ${(((run - 1 - failures) / (run - 1)) * 100).toFixed(1)}%`);
|
|
116
|
+
console.log('');
|
|
117
|
+
|
|
118
|
+
if (runTimes.length > 0) {
|
|
119
|
+
const avgTime = runTimes.reduce((a, b) => a + b, 0) / runTimes.length;
|
|
120
|
+
const minTime = Math.min(...runTimes);
|
|
121
|
+
const maxTime = Math.max(...runTimes);
|
|
122
|
+
|
|
123
|
+
console.log('⏱️ Timing Statistics');
|
|
124
|
+
console.log('=====================');
|
|
125
|
+
console.log(`Total time: ${(totalTestTime / 1000).toFixed(2)}s`);
|
|
126
|
+
console.log(`Average run time: ${(avgTime / 1000).toFixed(2)}s`);
|
|
127
|
+
console.log(`Fastest run: ${(minTime / 1000).toFixed(2)}s`);
|
|
128
|
+
console.log(`Slowest run: ${(maxTime / 1000).toFixed(2)}s`);
|
|
129
|
+
console.log('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (failures === 0) {
|
|
133
|
+
console.log(`🎉 All ${run - 1} runs passed successfully!`);
|
|
134
|
+
process.exit(0);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`💥 Test failed after ${run - 1} runs`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
package/temba-modules.ts
CHANGED
|
@@ -70,6 +70,7 @@ import { RangePicker } from './src/form/RangePicker';
|
|
|
70
70
|
import { NodeEditor } from './src/flow/NodeEditor';
|
|
71
71
|
import { KeyValueEditor } from './src/form/KeyValueEditor';
|
|
72
72
|
import { TembaArrayEditor } from './src/form/ArrayEditor';
|
|
73
|
+
import { MessageEditor } from './src/form/MessageEditor';
|
|
73
74
|
import './src/form/BaseListEditor'; // Import base class
|
|
74
75
|
|
|
75
76
|
export function addCustomElement(name: string, comp: any) {
|
|
@@ -151,3 +152,4 @@ addCustomElement('temba-workspace-select', WorkspaceSelect);
|
|
|
151
152
|
addCustomElement('temba-chart', TembaChart);
|
|
152
153
|
addCustomElement('temba-key-value-editor', KeyValueEditor);
|
|
153
154
|
addCustomElement('temba-array-editor', TembaArrayEditor);
|
|
155
|
+
addCustomElement('temba-message-editor', MessageEditor);
|
|
@@ -122,7 +122,8 @@ describe('Field Configuration System', () => {
|
|
|
122
122
|
await (el as any).updateComplete;
|
|
123
123
|
|
|
124
124
|
expect(el).to.exist;
|
|
125
|
-
|
|
125
|
+
// ArrayEditor with maintainEmptyItem=true doesn't show add button
|
|
126
|
+
expect(el.shadowRoot?.querySelector('.add-btn')).to.not.exist;
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
it('should render with initial items', async () => {
|
|
@@ -146,7 +147,8 @@ describe('Field Configuration System', () => {
|
|
|
146
147
|
|
|
147
148
|
expect(el).to.exist;
|
|
148
149
|
const items = el.shadowRoot?.querySelectorAll('.array-item');
|
|
149
|
-
|
|
150
|
+
// Expects 3 items: 2 initial items + 1 auto-generated empty item
|
|
151
|
+
expect(items?.length).to.equal(3);
|
|
150
152
|
});
|
|
151
153
|
});
|
|
152
154
|
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { fixture, assert, expect } from '@open-wc/testing';
|
|
2
|
+
import { MessageEditor } from '../src/form/MessageEditor';
|
|
3
|
+
import { assertScreenshot, getClip, getComponent } from './utils.test';
|
|
4
|
+
|
|
5
|
+
export const getHTML = (options: any = {}) => {
|
|
6
|
+
const attrs = Object.keys(options)
|
|
7
|
+
.map((key) => `${key}="${options[key]}"`)
|
|
8
|
+
.join(' ');
|
|
9
|
+
return `<temba-message-editor ${attrs}></temba-message-editor>`;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('temba-message-editor', () => {
|
|
13
|
+
it('can be created', async () => {
|
|
14
|
+
const editor: MessageEditor = await fixture(getHTML());
|
|
15
|
+
assert.instanceOf(editor, MessageEditor);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('has default properties', async () => {
|
|
19
|
+
const editor = (await getComponent(
|
|
20
|
+
'temba-message-editor'
|
|
21
|
+
)) as MessageEditor;
|
|
22
|
+
|
|
23
|
+
expect(editor.name).to.equal('');
|
|
24
|
+
expect(editor.value).to.equal('');
|
|
25
|
+
expect(editor.placeholder).to.equal('');
|
|
26
|
+
expect(editor.textarea).to.be.true;
|
|
27
|
+
expect(editor.autogrow).to.be.true;
|
|
28
|
+
expect(editor.minHeight).to.equal(60);
|
|
29
|
+
expect(editor.attachments).to.deep.equal([]);
|
|
30
|
+
expect(editor.maxAttachments).to.equal(3);
|
|
31
|
+
|
|
32
|
+
await assertScreenshot(
|
|
33
|
+
'message-editor/default',
|
|
34
|
+
getClip(editor as HTMLElement)
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('can set properties', async () => {
|
|
39
|
+
const editor = (await getComponent('temba-message-editor', {
|
|
40
|
+
name: 'message',
|
|
41
|
+
value: 'Hello world',
|
|
42
|
+
placeholder: 'Type your message...',
|
|
43
|
+
'max-attachments': '5'
|
|
44
|
+
})) as MessageEditor;
|
|
45
|
+
|
|
46
|
+
expect(editor.name).to.equal('message');
|
|
47
|
+
expect(editor.value).to.equal('Hello world');
|
|
48
|
+
expect(editor.placeholder).to.equal('Type your message...');
|
|
49
|
+
expect(editor.maxAttachments).to.equal(5);
|
|
50
|
+
|
|
51
|
+
await assertScreenshot(
|
|
52
|
+
'message-editor/with-properties',
|
|
53
|
+
getClip(editor as HTMLElement)
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders completion component', async () => {
|
|
58
|
+
const editor = (await getComponent('temba-message-editor', {
|
|
59
|
+
value: 'Test message',
|
|
60
|
+
placeholder: 'Enter message'
|
|
61
|
+
})) as MessageEditor;
|
|
62
|
+
|
|
63
|
+
const completion = editor.shadowRoot.querySelector(
|
|
64
|
+
'temba-completion'
|
|
65
|
+
) as any;
|
|
66
|
+
expect(completion).to.not.be.null;
|
|
67
|
+
expect(completion.hasAttribute('widgetOnly')).to.be.true;
|
|
68
|
+
|
|
69
|
+
await assertScreenshot(
|
|
70
|
+
'message-editor/with-completion',
|
|
71
|
+
getClip(editor as HTMLElement)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('filters runtime attachments', async () => {
|
|
76
|
+
const attachments = [
|
|
77
|
+
'image/jpeg:http://example.com/image.jpg',
|
|
78
|
+
'image:@fields.profile_pic',
|
|
79
|
+
'video:@fields.intro_video',
|
|
80
|
+
'application/pdf:http://example.com/doc.pdf'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const editor = (await getComponent('temba-message-editor', {
|
|
84
|
+
attachments: JSON.stringify(attachments)
|
|
85
|
+
})) as MessageEditor;
|
|
86
|
+
|
|
87
|
+
// Wait for component to update
|
|
88
|
+
await editor.updateComplete;
|
|
89
|
+
|
|
90
|
+
const mediaPicker = editor.shadowRoot.querySelector(
|
|
91
|
+
'temba-media-picker'
|
|
92
|
+
) as any;
|
|
93
|
+
expect(mediaPicker).to.not.be.null;
|
|
94
|
+
|
|
95
|
+
// Should only have the static attachments (those with '/' in content type)
|
|
96
|
+
expect(mediaPicker.attachments.length).to.equal(2);
|
|
97
|
+
expect(mediaPicker.attachments[0].content_type).to.equal('image/jpeg');
|
|
98
|
+
expect(mediaPicker.attachments[1].content_type).to.equal('application/pdf');
|
|
99
|
+
|
|
100
|
+
await assertScreenshot(
|
|
101
|
+
'message-editor/filtered-attachments',
|
|
102
|
+
getClip(editor as HTMLElement)
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('handles completion change events', async () => {
|
|
107
|
+
const editor = (await getComponent(
|
|
108
|
+
'temba-message-editor'
|
|
109
|
+
)) as MessageEditor;
|
|
110
|
+
let changeEvent: CustomEvent = null;
|
|
111
|
+
|
|
112
|
+
editor.addEventListener('change', (e: CustomEvent) => {
|
|
113
|
+
changeEvent = e;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const completion = editor.shadowRoot.querySelector(
|
|
117
|
+
'temba-completion'
|
|
118
|
+
) as any;
|
|
119
|
+
completion.value = 'New message';
|
|
120
|
+
completion.dispatchEvent(new Event('change'));
|
|
121
|
+
|
|
122
|
+
expect(editor.value).to.equal('New message');
|
|
123
|
+
expect(changeEvent).to.not.be.null;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles media picker change events', async () => {
|
|
127
|
+
const editor = (await getComponent(
|
|
128
|
+
'temba-message-editor'
|
|
129
|
+
)) as MessageEditor;
|
|
130
|
+
let changeEvent: CustomEvent = null;
|
|
131
|
+
|
|
132
|
+
editor.addEventListener('change', (e: CustomEvent) => {
|
|
133
|
+
changeEvent = e;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const mediaPicker = editor.shadowRoot.querySelector(
|
|
137
|
+
'temba-media-picker'
|
|
138
|
+
) as any;
|
|
139
|
+
mediaPicker.attachments = [
|
|
140
|
+
{
|
|
141
|
+
content_type: 'image/jpeg',
|
|
142
|
+
url: 'http://example.com/test.jpg',
|
|
143
|
+
filename: 'test.jpg',
|
|
144
|
+
size: 1024
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
mediaPicker.dispatchEvent(new Event('change'));
|
|
148
|
+
|
|
149
|
+
expect(editor.attachments).to.include(
|
|
150
|
+
'image/jpeg:http://example.com/test.jpg'
|
|
151
|
+
);
|
|
152
|
+
expect(changeEvent).to.not.be.null;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('preserves runtime attachments when media changes', async () => {
|
|
156
|
+
const initialAttachments = [
|
|
157
|
+
'image:@fields.profile_pic',
|
|
158
|
+
'image/jpeg:http://example.com/existing.jpg'
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const editor = (await getComponent('temba-message-editor', {
|
|
162
|
+
attachments: JSON.stringify(initialAttachments)
|
|
163
|
+
})) as MessageEditor;
|
|
164
|
+
|
|
165
|
+
await editor.updateComplete;
|
|
166
|
+
|
|
167
|
+
// Simulate media picker change
|
|
168
|
+
const mediaPicker = editor.shadowRoot.querySelector(
|
|
169
|
+
'temba-media-picker'
|
|
170
|
+
) as any;
|
|
171
|
+
mediaPicker.attachments = [
|
|
172
|
+
{
|
|
173
|
+
content_type: 'image/png',
|
|
174
|
+
url: 'http://example.com/new.png',
|
|
175
|
+
filename: 'new.png',
|
|
176
|
+
size: 2048
|
|
177
|
+
}
|
|
178
|
+
];
|
|
179
|
+
mediaPicker.dispatchEvent(new Event('change'));
|
|
180
|
+
|
|
181
|
+
// Should preserve runtime attachments and add new static ones
|
|
182
|
+
expect(editor.attachments).to.include('image:@fields.profile_pic');
|
|
183
|
+
expect(editor.attachments).to.include(
|
|
184
|
+
'image/png:http://example.com/new.png'
|
|
185
|
+
);
|
|
186
|
+
expect(editor.attachments.length).to.equal(2);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('supports drag and drop highlighting', async () => {
|
|
190
|
+
const editor = (await getComponent(
|
|
191
|
+
'temba-message-editor'
|
|
192
|
+
)) as MessageEditor;
|
|
193
|
+
const container = editor.shadowRoot.querySelector(
|
|
194
|
+
'.message-editor-container'
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Simulate drag enter
|
|
198
|
+
const dragEvent = new DragEvent('dragenter', {
|
|
199
|
+
bubbles: true,
|
|
200
|
+
dataTransfer: new DataTransfer()
|
|
201
|
+
});
|
|
202
|
+
container.dispatchEvent(dragEvent);
|
|
203
|
+
|
|
204
|
+
// Wait for the update
|
|
205
|
+
await editor.updateComplete;
|
|
206
|
+
|
|
207
|
+
expect(editor.pendingDrop).to.be.true;
|
|
208
|
+
expect(container.classList.contains('highlight')).to.be.true;
|
|
209
|
+
|
|
210
|
+
await assertScreenshot(
|
|
211
|
+
'message-editor/drag-highlight',
|
|
212
|
+
getClip(editor as HTMLElement)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Simulate drag leave
|
|
216
|
+
const dragLeaveEvent = new DragEvent('dragleave', {
|
|
217
|
+
bubbles: true,
|
|
218
|
+
dataTransfer: new DataTransfer()
|
|
219
|
+
});
|
|
220
|
+
container.dispatchEvent(dragLeaveEvent);
|
|
221
|
+
|
|
222
|
+
expect(editor.pendingDrop).to.be.false;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('focuses completion on focus', async () => {
|
|
226
|
+
const editor = (await getComponent(
|
|
227
|
+
'temba-message-editor'
|
|
228
|
+
)) as MessageEditor;
|
|
229
|
+
const completion = editor.shadowRoot.querySelector(
|
|
230
|
+
'temba-completion'
|
|
231
|
+
) as any;
|
|
232
|
+
|
|
233
|
+
let focusCalled = false;
|
|
234
|
+
completion.focus = () => {
|
|
235
|
+
focusCalled = true;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
editor.focus();
|
|
239
|
+
expect(focusCalled).to.be.true;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('clicks completion on click', async () => {
|
|
243
|
+
const editor = (await getComponent(
|
|
244
|
+
'temba-message-editor'
|
|
245
|
+
)) as MessageEditor;
|
|
246
|
+
const completion = editor.shadowRoot.querySelector(
|
|
247
|
+
'temba-completion'
|
|
248
|
+
) as any;
|
|
249
|
+
|
|
250
|
+
let clickCalled = false;
|
|
251
|
+
completion.click = () => {
|
|
252
|
+
clickCalled = true;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
editor.click();
|
|
256
|
+
expect(clickCalled).to.be.true;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('initializes with correct height for long text content', async () => {
|
|
260
|
+
const longText =
|
|
261
|
+
'This is a very long text that should span multiple lines and cause the autogrow functionality to kick in and expand the textarea to accommodate all the content. This text should be long enough to trigger the autogrow behavior during initialization.';
|
|
262
|
+
|
|
263
|
+
const editor = (await getComponent('temba-message-editor', {
|
|
264
|
+
value: longText,
|
|
265
|
+
'min-height': '60'
|
|
266
|
+
})) as MessageEditor;
|
|
267
|
+
|
|
268
|
+
// Wait for component to fully render
|
|
269
|
+
await editor.updateComplete;
|
|
270
|
+
|
|
271
|
+
// Get the text input element to verify its height
|
|
272
|
+
const completion = editor.shadowRoot.querySelector(
|
|
273
|
+
'temba-completion'
|
|
274
|
+
) as any;
|
|
275
|
+
expect(completion).to.not.be.null;
|
|
276
|
+
|
|
277
|
+
// The completion should have the long text value
|
|
278
|
+
expect(completion.value).to.equal(longText);
|
|
279
|
+
|
|
280
|
+
// Get the actual TextInput component inside the completion
|
|
281
|
+
const textInput = completion.getTextInput();
|
|
282
|
+
expect(textInput).to.not.be.null;
|
|
283
|
+
|
|
284
|
+
// The textarea should be in autogrow mode
|
|
285
|
+
expect(textInput.autogrow).to.be.true;
|
|
286
|
+
expect(textInput.textarea).to.be.true;
|
|
287
|
+
|
|
288
|
+
// Check that the autogrow div has been updated with content
|
|
289
|
+
const autogrowDiv = textInput.shadowRoot.querySelector(
|
|
290
|
+
'.grow-wrap > div'
|
|
291
|
+
) as HTMLDivElement;
|
|
292
|
+
expect(autogrowDiv).to.not.be.null;
|
|
293
|
+
expect(autogrowDiv.innerText).to.include(longText);
|
|
294
|
+
|
|
295
|
+
await assertScreenshot(
|
|
296
|
+
'message-editor/autogrow-initial-content',
|
|
297
|
+
getClip(editor as HTMLElement)
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -48,6 +48,34 @@ describe('temba-node-editor', () => {
|
|
|
48
48
|
expect(el.action).to.equal(action);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
it('renders send_msg action with message editor', async () => {
|
|
52
|
+
const action = {
|
|
53
|
+
uuid: 'test-action-uuid',
|
|
54
|
+
type: 'send_msg',
|
|
55
|
+
text: 'Hello @contact.name, check this out!',
|
|
56
|
+
attachments: [
|
|
57
|
+
'image/jpeg:http://example.com/photo.jpg',
|
|
58
|
+
'image:@fields.profile_pic'
|
|
59
|
+
],
|
|
60
|
+
quick_replies: ['Yes', 'No']
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const el = (await fixture(html`
|
|
64
|
+
<temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
|
|
65
|
+
`)) as NodeEditorElement;
|
|
66
|
+
|
|
67
|
+
await el.updateComplete;
|
|
68
|
+
expect(el.shadowRoot).to.not.be.null;
|
|
69
|
+
expect(el.action).to.equal(action);
|
|
70
|
+
|
|
71
|
+
// Check that the message editor component is rendered
|
|
72
|
+
const messageEditor = el.shadowRoot.querySelector(
|
|
73
|
+
'temba-message-editor'
|
|
74
|
+
) as any;
|
|
75
|
+
expect(messageEditor).to.not.be.null;
|
|
76
|
+
expect(messageEditor.value).to.equal(action.text);
|
|
77
|
+
});
|
|
78
|
+
|
|
51
79
|
it('renders set_run_result action', async () => {
|
|
52
80
|
const action = {
|
|
53
81
|
uuid: 'test-action-uuid',
|
|
@@ -350,4 +378,70 @@ describe('temba-node-editor', () => {
|
|
|
350
378
|
await assertDialogScreenshot(el, `editor/${actionType.type}`);
|
|
351
379
|
}
|
|
352
380
|
});
|
|
381
|
+
|
|
382
|
+
it('displays bubble count for group value counts', async () => {
|
|
383
|
+
const action = {
|
|
384
|
+
uuid: 'test-action-uuid',
|
|
385
|
+
type: 'send_msg',
|
|
386
|
+
text: 'Hello world',
|
|
387
|
+
quick_replies: ['Yes', 'No', 'Maybe'],
|
|
388
|
+
attachments: ['image:@contact.photo', 'document:@contact.resume']
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const el = (await fixture(html`
|
|
392
|
+
<temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
|
|
393
|
+
`)) as NodeEditorElement;
|
|
394
|
+
|
|
395
|
+
await el.updateComplete;
|
|
396
|
+
|
|
397
|
+
// Wait for form data to be fully initialized and re-render to complete
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
399
|
+
await el.updateComplete;
|
|
400
|
+
|
|
401
|
+
// Check that bubble counts are displayed
|
|
402
|
+
const shadowRoot = el.shadowRoot;
|
|
403
|
+
const bubbles = shadowRoot.querySelectorAll('.group-count-bubble');
|
|
404
|
+
|
|
405
|
+
// Should have bubbles for groups with values
|
|
406
|
+
expect(bubbles.length).to.be.greaterThan(0);
|
|
407
|
+
|
|
408
|
+
// Check specific bubble values (trim to handle whitespace in rendered text)
|
|
409
|
+
const bubbleTexts = Array.from(bubbles).map((bubble) =>
|
|
410
|
+
bubble.textContent?.trim()
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Runtime attachments group should show bubble when collapsed and has values
|
|
414
|
+
expect(bubbleTexts).to.include('2'); // 2 runtime attachments
|
|
415
|
+
// Note: Quick replies group auto-expands when it has content, so no bubble is shown
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('shows arrow when group has no values', async () => {
|
|
419
|
+
const action = {
|
|
420
|
+
uuid: 'test-action-uuid',
|
|
421
|
+
type: 'send_msg',
|
|
422
|
+
text: 'Hello world'
|
|
423
|
+
// No quick_replies or attachments provided
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const el = (await fixture(html`
|
|
427
|
+
<temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
|
|
428
|
+
`)) as NodeEditorElement;
|
|
429
|
+
|
|
430
|
+
await el.updateComplete;
|
|
431
|
+
|
|
432
|
+
// Wait for form data initialization
|
|
433
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
434
|
+
await el.updateComplete;
|
|
435
|
+
|
|
436
|
+
// Check that arrows are displayed instead of bubbles
|
|
437
|
+
const shadowRoot = el.shadowRoot;
|
|
438
|
+
const bubbles = shadowRoot.querySelectorAll('.group-count-bubble');
|
|
439
|
+
const arrows = shadowRoot.querySelectorAll('.group-toggle-icon');
|
|
440
|
+
|
|
441
|
+
// Should have no bubbles when counts are 0
|
|
442
|
+
expect(bubbles.length).to.equal(0);
|
|
443
|
+
|
|
444
|
+
// Should have arrows for collapsible groups
|
|
445
|
+
expect(arrows.length).to.be.greaterThan(0);
|
|
446
|
+
});
|
|
353
447
|
});
|
|
@@ -927,7 +927,7 @@ describe('temba-select', () => {
|
|
|
927
927
|
|
|
928
928
|
await openSelect(clock, select);
|
|
929
929
|
// Cached results should be available immediately, but give some time for rendering
|
|
930
|
-
await waitForSelectPagination(select, clock, 15,
|
|
930
|
+
await waitForSelectPagination(select, clock, 15, 10);
|
|
931
931
|
assert.equal(select.visibleOptions.length, 15);
|
|
932
932
|
|
|
933
933
|
// close and reopen once more (previous bug failed on third opening)
|
|
@@ -197,4 +197,30 @@ describe('temba-textinput', () => {
|
|
|
197
197
|
await assertScreenshot('textinput/input-updated', getClip(input));
|
|
198
198
|
expect(widget.value).to.equal('Updated by attribute change');
|
|
199
199
|
});
|
|
200
|
+
|
|
201
|
+
it('initializes autogrow with content', async () => {
|
|
202
|
+
const longText =
|
|
203
|
+
'This is a very long text that should span multiple lines and cause the autogrow functionality to kick in and expand the textarea to accommodate all the content during initialization.';
|
|
204
|
+
|
|
205
|
+
const input: TextInput = await createInput(
|
|
206
|
+
getInputHTML({
|
|
207
|
+
value: longText,
|
|
208
|
+
textarea: true,
|
|
209
|
+
autogrow: true
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Wait for component to fully render
|
|
214
|
+
await input.updateComplete;
|
|
215
|
+
|
|
216
|
+
// Check that autogrow div has been updated with initial content
|
|
217
|
+
const autogrowDiv = input.shadowRoot.querySelector(
|
|
218
|
+
'.grow-wrap > div'
|
|
219
|
+
) as HTMLDivElement;
|
|
220
|
+
expect(autogrowDiv).to.not.be.null;
|
|
221
|
+
expect(autogrowDiv.innerText).to.include(longText);
|
|
222
|
+
expect(autogrowDiv.innerText).to.include('\n'); // Should have the newline character added
|
|
223
|
+
|
|
224
|
+
await assertScreenshot('textinput/autogrow-initial', getClip(input));
|
|
225
|
+
});
|
|
200
226
|
});
|
|
@@ -16,6 +16,11 @@ const TAG = 'temba-webchat';
|
|
|
16
16
|
const getWebChat = async (attrs: any = {}) => {
|
|
17
17
|
const webChat = (await getComponent(TAG, attrs, '', 400, 600)) as WebChat;
|
|
18
18
|
|
|
19
|
+
// Ensure component is fully initialized before returning
|
|
20
|
+
await webChat.updateComplete;
|
|
21
|
+
clock.tick(100);
|
|
22
|
+
await webChat.updateComplete;
|
|
23
|
+
|
|
19
24
|
return webChat;
|
|
20
25
|
};
|
|
21
26
|
|
package/test/utils.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ interface Clip {
|
|
|
7
7
|
height: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
import { expect, fixture, html, assert
|
|
10
|
+
import { expect, fixture, html, assert } from '@open-wc/testing';
|
|
11
11
|
import MouseHelper from './MouseHelper';
|
|
12
12
|
import { Store } from '../src/store/Store';
|
|
13
13
|
import { stub } from 'sinon';
|
|
@@ -200,18 +200,7 @@ export const waitForCondition = async (
|
|
|
200
200
|
}
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
export const assertScreenshot = async (
|
|
204
|
-
filename: string,
|
|
205
|
-
clip: Clip,
|
|
206
|
-
waitFor?: { clock?: any; predicate?: () => boolean }
|
|
207
|
-
) => {
|
|
208
|
-
if (waitFor) {
|
|
209
|
-
if (waitFor.clock) {
|
|
210
|
-
waitFor.clock.restore();
|
|
211
|
-
}
|
|
212
|
-
await waitUntil(waitFor.predicate);
|
|
213
|
-
}
|
|
214
|
-
|
|
203
|
+
export const assertScreenshot = async (filename: string, clip: Clip) => {
|
|
215
204
|
// detect if we're running in copilot's environment and use adaptive threshold
|
|
216
205
|
const isCopilotEnvironment = (window as any).isCopilotEnvironment;
|
|
217
206
|
const threshold = isCopilotEnvironment ? 1.0 : 0.1;
|
|
@@ -217,6 +217,25 @@
|
|
|
217
217
|
},
|
|
218
218
|
"logs_url": null
|
|
219
219
|
},
|
|
220
|
+
{
|
|
221
|
+
"type": "run_started",
|
|
222
|
+
"created_on": "2021-03-30T22:20:26.704467+00:00",
|
|
223
|
+
"run_uuid": "0198c846-2cf4-7992-9bd0-9b6581e42358",
|
|
224
|
+
"flow": {
|
|
225
|
+
"uuid": "d076d716-8071-417e-b188-d9746db223a1",
|
|
226
|
+
"name": "Favorites"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"type": "run_ended",
|
|
231
|
+
"created_on": "2021-03-30T22:20:26.704467+00:00",
|
|
232
|
+
"run_uuid": "0198c846-da2b-799f-8af9-aaf8857f947f",
|
|
233
|
+
"flow": {
|
|
234
|
+
"uuid": "d076d716-8071-417e-b188-d9746db223a1",
|
|
235
|
+
"name": "Favorites"
|
|
236
|
+
},
|
|
237
|
+
"status": "completed"
|
|
238
|
+
},
|
|
220
239
|
{
|
|
221
240
|
"uuid": "01988a77-979e-7768-a940-8d9c348e24fe",
|
|
222
241
|
"type": "msg_received",
|