@nyaruka/temba-components 0.135.9 → 0.136.0
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 +16 -0
- package/demo/components/webchat/example.html +4 -2
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +1323 -317
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +2 -1
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +11 -0
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +224 -2
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +320 -1
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +30 -8
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1827 -0
- package/out-tsc/src/simulator/Simulator.js.map +1 -0
- package/out-tsc/src/store/AppState.js +33 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils.js +48 -0
- package/out-tsc/src/utils.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-flow-editor.test.js +1 -1
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +642 -0
- package/out-tsc/test/temba-simulator.test.js.map +1 -0
- package/out-tsc/test/utils.test.js +1 -1
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/simulator/after-message-sent.png +0 -0
- package/screenshots/truth/simulator/after-reset.png +0 -0
- package/screenshots/truth/simulator/attachment-menu.png +0 -0
- package/screenshots/truth/simulator/context-expanded.png +0 -0
- package/screenshots/truth/simulator/context-explorer-open.png +0 -0
- package/screenshots/truth/simulator/event-info.png +0 -0
- package/screenshots/truth/simulator/image-attachment.png +0 -0
- package/screenshots/truth/simulator/open-initial.png +0 -0
- package/screenshots/truth/simulator/quick-replies.png +0 -0
- package/src/Icons.ts +2 -1
- package/src/flow/CanvasNode.ts +12 -0
- package/src/flow/Editor.ts +240 -1
- package/src/flow/Plumber.ts +371 -2
- package/src/interfaces.ts +2 -1
- package/src/layout/FloatingWindow.ts +36 -11
- package/src/simulator/Simulator.ts +2008 -0
- package/src/store/AppState.ts +53 -0
- package/src/utils.ts +53 -0
- package/static/svg/index.svg +1 -1
- package/static/svg/work/traced/route.svg +1 -0
- package/static/svg/work/used/route.svg +3 -0
- package/temba-modules.ts +2 -0
- package/test/temba-flow-editor.test.ts +1 -1
- package/test/temba-flow-plumber-connections.test.ts +4 -1
- package/test/temba-flow-plumber.test.ts +4 -1
- package/test/temba-simulator.test.ts +866 -0
- package/test/utils.test.ts +1 -1
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { fixture, expect, assert } from '@open-wc/testing';
|
|
2
|
+
import { Simulator } from '../src/simulator/Simulator';
|
|
3
|
+
import { assertScreenshot, getClip, mockPOST, clearMockPosts, delay, waitForCondition, loadStore } from './utils.test';
|
|
4
|
+
const FLOW_UUID = 'test-flow-123';
|
|
5
|
+
const createSimulator = async (attrs = {}) => {
|
|
6
|
+
// load store first since simulator depends on it
|
|
7
|
+
await loadStore();
|
|
8
|
+
const defaults = {
|
|
9
|
+
flow: FLOW_UUID,
|
|
10
|
+
animationTime: '0' // disable animations for deterministic tests
|
|
11
|
+
};
|
|
12
|
+
const mergedAttrs = { ...defaults, ...attrs };
|
|
13
|
+
const attrString = Object.entries(mergedAttrs)
|
|
14
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
15
|
+
.join(' ');
|
|
16
|
+
const simulator = await fixture(`<temba-simulator ${attrString}></temba-simulator>`);
|
|
17
|
+
// reset cookie-based properties for deterministic tests
|
|
18
|
+
simulator.size = 'medium';
|
|
19
|
+
simulator.following = true;
|
|
20
|
+
simulator.contextExplorerOpen = false;
|
|
21
|
+
await simulator.updateComplete;
|
|
22
|
+
return simulator;
|
|
23
|
+
};
|
|
24
|
+
// helper to open the simulator
|
|
25
|
+
const openSimulator = async (simulator) => {
|
|
26
|
+
const tab = simulator.shadowRoot.querySelector('temba-floating-tab');
|
|
27
|
+
expect(tab).to.exist;
|
|
28
|
+
// trigger the button clicked event on the tab
|
|
29
|
+
tab.dispatchEvent(new CustomEvent('temba-button-clicked', { bubbles: true }));
|
|
30
|
+
await simulator.updateComplete;
|
|
31
|
+
// brief delay for async API mock processing
|
|
32
|
+
await delay(50);
|
|
33
|
+
};
|
|
34
|
+
// helper to get clip for the simulator window (fixed positioning)
|
|
35
|
+
const getSimulatorClip = (simulator, includeContext = false) => {
|
|
36
|
+
var _a;
|
|
37
|
+
const phoneWindow = simulator.shadowRoot.querySelector('temba-floating-window');
|
|
38
|
+
if (!phoneWindow) {
|
|
39
|
+
// if window not open, use default clip
|
|
40
|
+
return getClip(simulator);
|
|
41
|
+
}
|
|
42
|
+
const windowElement = (_a = phoneWindow.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.window');
|
|
43
|
+
if (!windowElement) {
|
|
44
|
+
return getClip(simulator);
|
|
45
|
+
}
|
|
46
|
+
const windowBounds = windowElement.getBoundingClientRect();
|
|
47
|
+
if (includeContext) {
|
|
48
|
+
// get the context explorer and phone to clip just those areas
|
|
49
|
+
const phoneSimulator = phoneWindow.querySelector('.phone-simulator');
|
|
50
|
+
if (!phoneSimulator) {
|
|
51
|
+
return getClip(simulator);
|
|
52
|
+
}
|
|
53
|
+
const contextExplorer = phoneSimulator.querySelector('.context-explorer');
|
|
54
|
+
const phoneFrame = phoneSimulator.querySelector('.phone-frame');
|
|
55
|
+
if (!contextExplorer || !phoneFrame) {
|
|
56
|
+
return {
|
|
57
|
+
x: windowBounds.x,
|
|
58
|
+
y: windowBounds.y,
|
|
59
|
+
width: windowBounds.width,
|
|
60
|
+
height: windowBounds.height
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const contextBounds = contextExplorer.getBoundingClientRect();
|
|
64
|
+
const phoneBounds = phoneFrame.getBoundingClientRect();
|
|
65
|
+
// clip from the left edge of context explorer to the right edge of phone frame only
|
|
66
|
+
// do not include the option-pane which is to the right of the phone
|
|
67
|
+
// keep padding within the phone bounds to avoid capturing the gap to the option pane
|
|
68
|
+
const padding = 10;
|
|
69
|
+
const leftX = contextBounds.x - padding;
|
|
70
|
+
// don't extend past the phone frame right edge - the option pane is close by
|
|
71
|
+
const rightX = phoneBounds.right;
|
|
72
|
+
const topY = Math.min(contextBounds.y, phoneBounds.y) - padding;
|
|
73
|
+
const bottomY = Math.max(contextBounds.bottom, phoneBounds.bottom) + padding;
|
|
74
|
+
return {
|
|
75
|
+
x: leftX,
|
|
76
|
+
y: topY,
|
|
77
|
+
width: rightX - leftX,
|
|
78
|
+
height: bottomY - topY
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// the phone-simulator is in the light DOM of the phoneWindow (slotted content)
|
|
82
|
+
const phoneSimulator = phoneWindow.querySelector('.phone-simulator');
|
|
83
|
+
if (!phoneSimulator) {
|
|
84
|
+
return getClip(simulator);
|
|
85
|
+
}
|
|
86
|
+
// get the phone-frame from within the phone-simulator
|
|
87
|
+
const phoneFrame = phoneSimulator.querySelector('.phone-frame');
|
|
88
|
+
if (!phoneFrame) {
|
|
89
|
+
// fallback to window bounds if phone-frame not found
|
|
90
|
+
return {
|
|
91
|
+
x: windowBounds.x,
|
|
92
|
+
y: windowBounds.y,
|
|
93
|
+
width: windowBounds.width,
|
|
94
|
+
height: windowBounds.height
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const frameBounds = phoneFrame.getBoundingClientRect();
|
|
98
|
+
// add padding around the phone frame
|
|
99
|
+
const padding = 10;
|
|
100
|
+
return {
|
|
101
|
+
x: frameBounds.x - padding,
|
|
102
|
+
y: frameBounds.y - padding,
|
|
103
|
+
width: frameBounds.width + padding * 2,
|
|
104
|
+
height: frameBounds.height + padding * 2
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
// mock responses for simulation endpoints
|
|
108
|
+
const mockSimulatorStart = () => {
|
|
109
|
+
const response = {
|
|
110
|
+
session: {
|
|
111
|
+
status: 'waiting',
|
|
112
|
+
trigger: {
|
|
113
|
+
type: 'manual',
|
|
114
|
+
flow: { uuid: FLOW_UUID, name: 'Test Flow' }
|
|
115
|
+
},
|
|
116
|
+
runs: [
|
|
117
|
+
{
|
|
118
|
+
uuid: 'run-1',
|
|
119
|
+
flow: { uuid: FLOW_UUID, name: 'Test Flow' },
|
|
120
|
+
status: 'waiting',
|
|
121
|
+
path: [
|
|
122
|
+
{
|
|
123
|
+
uuid: 'step-1',
|
|
124
|
+
node_uuid: 'node-1',
|
|
125
|
+
arrived_on: new Date().toISOString(),
|
|
126
|
+
exit_uuid: null
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
environment: {
|
|
132
|
+
date_format: 'YYYY-MM-DD',
|
|
133
|
+
time_format: 'HH:mm',
|
|
134
|
+
timezone: 'America/New_York',
|
|
135
|
+
allowed_languages: ['eng'],
|
|
136
|
+
default_country: 'US'
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
events: [
|
|
140
|
+
{
|
|
141
|
+
type: 'msg_created',
|
|
142
|
+
created_on: new Date().toISOString(),
|
|
143
|
+
msg: {
|
|
144
|
+
uuid: 'msg-1',
|
|
145
|
+
text: 'Hello! How can I help you today?',
|
|
146
|
+
urn: 'tel:+12065551212'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
contact: {
|
|
151
|
+
uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
|
|
152
|
+
urns: ['tel:+12065551212'],
|
|
153
|
+
fields: {},
|
|
154
|
+
groups: [],
|
|
155
|
+
language: 'eng',
|
|
156
|
+
status: 'active',
|
|
157
|
+
created_on: new Date().toISOString()
|
|
158
|
+
},
|
|
159
|
+
context: {
|
|
160
|
+
contact: {
|
|
161
|
+
uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
|
|
162
|
+
name: 'Test User',
|
|
163
|
+
urns: {
|
|
164
|
+
tel: ['+12065551212'],
|
|
165
|
+
__default__: '+12065551212'
|
|
166
|
+
},
|
|
167
|
+
fields: {
|
|
168
|
+
age: '25',
|
|
169
|
+
city: 'Seattle'
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
trigger: {
|
|
173
|
+
type: 'manual',
|
|
174
|
+
__default__: 'manual'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
mockPOST(/\/flow\/simulate\/.*\//, response);
|
|
179
|
+
};
|
|
180
|
+
const mockSimulatorResume = (responseText, quickReplies) => {
|
|
181
|
+
const msg = {
|
|
182
|
+
uuid: 'msg-response',
|
|
183
|
+
text: responseText,
|
|
184
|
+
urn: 'tel:+12065551212'
|
|
185
|
+
};
|
|
186
|
+
if (quickReplies) {
|
|
187
|
+
msg.quick_replies = quickReplies.map((text) => ({ text }));
|
|
188
|
+
}
|
|
189
|
+
const response = {
|
|
190
|
+
session: {
|
|
191
|
+
status: 'waiting',
|
|
192
|
+
trigger: {
|
|
193
|
+
type: 'manual',
|
|
194
|
+
flow: { uuid: FLOW_UUID, name: 'Test Flow' }
|
|
195
|
+
},
|
|
196
|
+
runs: [
|
|
197
|
+
{
|
|
198
|
+
uuid: 'run-1',
|
|
199
|
+
flow: { uuid: FLOW_UUID, name: 'Test Flow' },
|
|
200
|
+
status: 'waiting',
|
|
201
|
+
path: [
|
|
202
|
+
{
|
|
203
|
+
uuid: 'step-1',
|
|
204
|
+
node_uuid: 'node-1',
|
|
205
|
+
arrived_on: new Date().toISOString(),
|
|
206
|
+
exit_uuid: 'exit-1'
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
uuid: 'step-2',
|
|
210
|
+
node_uuid: 'node-2',
|
|
211
|
+
arrived_on: new Date().toISOString(),
|
|
212
|
+
exit_uuid: null
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
environment: {
|
|
218
|
+
date_format: 'YYYY-MM-DD',
|
|
219
|
+
time_format: 'HH:mm',
|
|
220
|
+
timezone: 'America/New_York',
|
|
221
|
+
allowed_languages: ['eng'],
|
|
222
|
+
default_country: 'US'
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
events: [
|
|
226
|
+
{
|
|
227
|
+
type: 'msg_created',
|
|
228
|
+
created_on: new Date().toISOString(),
|
|
229
|
+
msg
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
contact: {
|
|
233
|
+
uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
|
|
234
|
+
urns: ['tel:+12065551212'],
|
|
235
|
+
fields: {
|
|
236
|
+
age: '25',
|
|
237
|
+
city: 'Seattle'
|
|
238
|
+
},
|
|
239
|
+
groups: [],
|
|
240
|
+
language: 'eng',
|
|
241
|
+
status: 'active',
|
|
242
|
+
created_on: new Date().toISOString()
|
|
243
|
+
},
|
|
244
|
+
context: {
|
|
245
|
+
contact: {
|
|
246
|
+
uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
|
|
247
|
+
name: 'Test User',
|
|
248
|
+
urns: {
|
|
249
|
+
tel: ['+12065551212'],
|
|
250
|
+
__default__: '+12065551212'
|
|
251
|
+
},
|
|
252
|
+
fields: {
|
|
253
|
+
age: '25',
|
|
254
|
+
city: 'Seattle'
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
results: {
|
|
258
|
+
user_response: {
|
|
259
|
+
value: responseText,
|
|
260
|
+
__default__: responseText
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
mockPOST(/\/flow\/simulate\/.*\//, response);
|
|
266
|
+
};
|
|
267
|
+
describe('temba-simulator', () => {
|
|
268
|
+
beforeEach(() => {
|
|
269
|
+
clearMockPosts();
|
|
270
|
+
});
|
|
271
|
+
it('can be created', async () => {
|
|
272
|
+
const simulator = await createSimulator();
|
|
273
|
+
assert.instanceOf(simulator, Simulator);
|
|
274
|
+
expect(simulator.flow).to.equal(FLOW_UUID);
|
|
275
|
+
expect(simulator.endpoint).to.equal(`/flow/simulate/${FLOW_UUID}/`);
|
|
276
|
+
});
|
|
277
|
+
it('opens simulator window and starts flow', async () => {
|
|
278
|
+
mockSimulatorStart();
|
|
279
|
+
const simulator = await createSimulator();
|
|
280
|
+
// ensure consistent size for screenshot
|
|
281
|
+
simulator.size = 'medium';
|
|
282
|
+
await simulator.updateComplete;
|
|
283
|
+
await openSimulator(simulator);
|
|
284
|
+
const phoneWindow = simulator.shadowRoot.querySelector('temba-floating-window');
|
|
285
|
+
expect(phoneWindow).to.exist;
|
|
286
|
+
// verify phone screen is visible
|
|
287
|
+
const phoneScreen = simulator.shadowRoot.querySelector('.phone-screen');
|
|
288
|
+
expect(phoneScreen).to.exist;
|
|
289
|
+
// verify initial message is displayed
|
|
290
|
+
const messages = simulator.shadowRoot.querySelectorAll('.message');
|
|
291
|
+
expect(messages.length).to.be.greaterThan(0);
|
|
292
|
+
await assertScreenshot('simulator/open-initial', getSimulatorClip(simulator));
|
|
293
|
+
});
|
|
294
|
+
it('sends a text message', async () => {
|
|
295
|
+
mockSimulatorStart();
|
|
296
|
+
const simulator = await createSimulator();
|
|
297
|
+
simulator.size = 'medium';
|
|
298
|
+
await simulator.updateComplete;
|
|
299
|
+
await openSimulator(simulator);
|
|
300
|
+
// count initial messages
|
|
301
|
+
let messages = simulator.shadowRoot.querySelectorAll('.message');
|
|
302
|
+
const initialCount = messages.length;
|
|
303
|
+
// mock the resume response
|
|
304
|
+
mockSimulatorResume('Thanks for your message!');
|
|
305
|
+
// type a message
|
|
306
|
+
const input = simulator.shadowRoot.querySelector('.message-input input');
|
|
307
|
+
expect(input).to.exist;
|
|
308
|
+
input.value = 'Hello from test';
|
|
309
|
+
input.dispatchEvent(new Event('input'));
|
|
310
|
+
await simulator.updateComplete;
|
|
311
|
+
// press enter to send
|
|
312
|
+
const enterEvent = new KeyboardEvent('keyup', {
|
|
313
|
+
key: 'Enter',
|
|
314
|
+
bubbles: true
|
|
315
|
+
});
|
|
316
|
+
input.dispatchEvent(enterEvent);
|
|
317
|
+
await simulator.updateComplete;
|
|
318
|
+
// brief delay for async API mock processing
|
|
319
|
+
await delay(100);
|
|
320
|
+
// verify we have more messages than before
|
|
321
|
+
messages = simulator.shadowRoot.querySelectorAll('.message');
|
|
322
|
+
expect(messages.length).to.be.greaterThan(initialCount);
|
|
323
|
+
// ensure DOM is settled
|
|
324
|
+
await simulator.updateComplete;
|
|
325
|
+
await assertScreenshot('simulator/after-message-sent', getSimulatorClip(simulator));
|
|
326
|
+
});
|
|
327
|
+
it('tests message flow and takes screenshot', async () => {
|
|
328
|
+
mockSimulatorStart();
|
|
329
|
+
const simulator = await createSimulator();
|
|
330
|
+
simulator.size = 'medium';
|
|
331
|
+
await simulator.updateComplete;
|
|
332
|
+
await openSimulator(simulator);
|
|
333
|
+
// clear previous mocks and set up new mock for a response
|
|
334
|
+
clearMockPosts();
|
|
335
|
+
mockSimulatorResume('Thank you for your message!', ['Yes', 'No', 'Maybe']);
|
|
336
|
+
// send a message
|
|
337
|
+
const input = simulator.shadowRoot.querySelector('.message-input input');
|
|
338
|
+
input.value = 'Test message';
|
|
339
|
+
input.dispatchEvent(new Event('input'));
|
|
340
|
+
const enterEvent = new KeyboardEvent('keyup', {
|
|
341
|
+
key: 'Enter',
|
|
342
|
+
bubbles: true
|
|
343
|
+
});
|
|
344
|
+
input.dispatchEvent(enterEvent);
|
|
345
|
+
// wait for quick replies to appear
|
|
346
|
+
await waitForCondition(() => simulator.shadowRoot.querySelectorAll('.quick-reply-btn').length > 0, 2000);
|
|
347
|
+
await simulator.updateComplete;
|
|
348
|
+
// take screenshot with quick replies
|
|
349
|
+
await assertScreenshot('simulator/quick-replies', getSimulatorClip(simulator));
|
|
350
|
+
});
|
|
351
|
+
it('opens attachment menu', async () => {
|
|
352
|
+
mockSimulatorStart();
|
|
353
|
+
const simulator = await createSimulator();
|
|
354
|
+
simulator.size = 'medium';
|
|
355
|
+
await simulator.updateComplete;
|
|
356
|
+
await openSimulator(simulator);
|
|
357
|
+
// click the attachment button
|
|
358
|
+
const attachmentButton = simulator.shadowRoot.querySelector('.attachment-button');
|
|
359
|
+
expect(attachmentButton).to.exist;
|
|
360
|
+
attachmentButton.click();
|
|
361
|
+
await simulator.updateComplete;
|
|
362
|
+
// verify attachment menu is displayed
|
|
363
|
+
const attachmentMenu = simulator.shadowRoot.querySelector('.attachment-menu');
|
|
364
|
+
expect(attachmentMenu).to.exist;
|
|
365
|
+
expect(attachmentMenu.classList.contains('open')).to.be.true;
|
|
366
|
+
await assertScreenshot('simulator/attachment-menu', getSimulatorClip(simulator));
|
|
367
|
+
});
|
|
368
|
+
it('sends an image attachment', async () => {
|
|
369
|
+
mockSimulatorStart();
|
|
370
|
+
const simulator = await createSimulator();
|
|
371
|
+
simulator.size = 'medium';
|
|
372
|
+
await simulator.updateComplete;
|
|
373
|
+
// reset attachment indices for deterministic testing
|
|
374
|
+
simulator.resetAttachmentIndices();
|
|
375
|
+
await openSimulator(simulator);
|
|
376
|
+
// mock the response for image attachment
|
|
377
|
+
mockSimulatorResume('Nice picture!');
|
|
378
|
+
// open attachment menu and click image option
|
|
379
|
+
const attachmentButton = simulator.shadowRoot.querySelector('.attachment-button');
|
|
380
|
+
attachmentButton.click();
|
|
381
|
+
await simulator.updateComplete;
|
|
382
|
+
await delay(200);
|
|
383
|
+
const imageMenuItem = Array.from(simulator.shadowRoot.querySelectorAll('.attachment-menu-item')).find((el) => { var _a; return (_a = el.textContent) === null || _a === void 0 ? void 0 : _a.includes('Image'); });
|
|
384
|
+
expect(imageMenuItem).to.exist;
|
|
385
|
+
imageMenuItem.click();
|
|
386
|
+
await delay(100);
|
|
387
|
+
await simulator.updateComplete;
|
|
388
|
+
// verify attachment wrapper is displayed (image attachments show in attachments not messages)
|
|
389
|
+
const attachmentWrappers = simulator.shadowRoot.querySelectorAll('.attachment-wrapper');
|
|
390
|
+
expect(attachmentWrappers.length).to.be.greaterThan(0);
|
|
391
|
+
await assertScreenshot('simulator/image-attachment', getSimulatorClip(simulator));
|
|
392
|
+
});
|
|
393
|
+
it('opens context explorer', async () => {
|
|
394
|
+
mockSimulatorStart();
|
|
395
|
+
const simulator = await createSimulator();
|
|
396
|
+
await openSimulator(simulator);
|
|
397
|
+
// find and click the context explorer button (has expressions icon)
|
|
398
|
+
const optionButtons = Array.from(simulator.shadowRoot.querySelectorAll('.option-btn'));
|
|
399
|
+
const contextButton = optionButtons.find((btn) => btn.querySelector('temba-icon[name="expressions"]'));
|
|
400
|
+
expect(contextButton).to.exist;
|
|
401
|
+
contextButton.click();
|
|
402
|
+
await simulator.updateComplete;
|
|
403
|
+
await delay(100);
|
|
404
|
+
// verify context explorer is displayed
|
|
405
|
+
const contextExplorer = simulator.shadowRoot.querySelector('.context-explorer');
|
|
406
|
+
expect(contextExplorer).to.exist;
|
|
407
|
+
expect(contextExplorer.classList.contains('open')).to.be.true;
|
|
408
|
+
// delay for context explorer to fully render
|
|
409
|
+
await delay(300);
|
|
410
|
+
await simulator.updateComplete;
|
|
411
|
+
await document.fonts.ready;
|
|
412
|
+
await assertScreenshot('simulator/context-explorer-open', getSimulatorClip(simulator, true));
|
|
413
|
+
});
|
|
414
|
+
it('expands context tree items', async () => {
|
|
415
|
+
mockSimulatorStart();
|
|
416
|
+
const simulator = await createSimulator();
|
|
417
|
+
await openSimulator(simulator);
|
|
418
|
+
// ensure context explorer starts closed
|
|
419
|
+
if (simulator.contextExplorerOpen) {
|
|
420
|
+
// click to close it first
|
|
421
|
+
const optionButtons = Array.from(simulator.shadowRoot.querySelectorAll('.option-btn'));
|
|
422
|
+
const contextButton = optionButtons.find((btn) => btn.querySelector('temba-icon[name="expressions"]'));
|
|
423
|
+
contextButton.click();
|
|
424
|
+
await simulator.updateComplete;
|
|
425
|
+
await delay(100);
|
|
426
|
+
}
|
|
427
|
+
// now open context explorer
|
|
428
|
+
const optionButtons = Array.from(simulator.shadowRoot.querySelectorAll('.option-btn'));
|
|
429
|
+
const contextButton = optionButtons.find((btn) => btn.querySelector('temba-icon[name="expressions"]'));
|
|
430
|
+
expect(contextButton).to.exist;
|
|
431
|
+
contextButton.click();
|
|
432
|
+
await simulator.updateComplete;
|
|
433
|
+
await delay(100);
|
|
434
|
+
// verify context explorer is now open
|
|
435
|
+
expect(simulator.contextExplorerOpen).to.be.true;
|
|
436
|
+
const contextExplorer = simulator.shadowRoot.querySelector('.context-explorer');
|
|
437
|
+
expect(contextExplorer).to.exist;
|
|
438
|
+
expect(contextExplorer.classList.contains('open')).to.be.true;
|
|
439
|
+
// find and click on an expandable item (should have context-item-expandable class)
|
|
440
|
+
const expandableItems = simulator.shadowRoot.querySelectorAll('.context-item-expandable');
|
|
441
|
+
expect(expandableItems.length).to.be.greaterThan(0);
|
|
442
|
+
const firstExpandable = expandableItems[0];
|
|
443
|
+
firstExpandable.click();
|
|
444
|
+
// wait for children to be displayed with specific content
|
|
445
|
+
await waitForCondition(() => {
|
|
446
|
+
const children = simulator.shadowRoot.querySelectorAll('.context-children');
|
|
447
|
+
if (children.length === 0)
|
|
448
|
+
return false;
|
|
449
|
+
// also check that the children have rendered content
|
|
450
|
+
const items = simulator.shadowRoot.querySelectorAll('.context-item');
|
|
451
|
+
return items.length > expandableItems.length;
|
|
452
|
+
}, 2000);
|
|
453
|
+
// verify children are displayed
|
|
454
|
+
const contextChildren = simulator.shadowRoot.querySelectorAll('.context-children');
|
|
455
|
+
expect(contextChildren.length).to.be.greaterThan(0);
|
|
456
|
+
await simulator.updateComplete;
|
|
457
|
+
// delay for DOM to fully render expanded content (context rendering is complex)
|
|
458
|
+
await delay(300);
|
|
459
|
+
await simulator.updateComplete;
|
|
460
|
+
// ensure fonts are loaded and give extra time for rendering
|
|
461
|
+
await document.fonts.ready;
|
|
462
|
+
// wait for any pending animation frames
|
|
463
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
464
|
+
await delay(200);
|
|
465
|
+
await assertScreenshot('simulator/context-expanded', getSimulatorClip(simulator, true));
|
|
466
|
+
});
|
|
467
|
+
it('cycles simulator size', async () => {
|
|
468
|
+
mockSimulatorStart();
|
|
469
|
+
const simulator = await createSimulator();
|
|
470
|
+
await openSimulator(simulator);
|
|
471
|
+
// initially should be medium (set in createSimulator)
|
|
472
|
+
expect(simulator.size).to.equal('medium');
|
|
473
|
+
// find and click the size button (shows current size as text)
|
|
474
|
+
const optionButtons = Array.from(simulator.shadowRoot.querySelectorAll('.option-btn'));
|
|
475
|
+
const sizeButton = optionButtons.find((btn) => {
|
|
476
|
+
var _a;
|
|
477
|
+
const text = (_a = btn.textContent) === null || _a === void 0 ? void 0 : _a.trim();
|
|
478
|
+
return text === 'S' || text === 'M' || text === 'L';
|
|
479
|
+
});
|
|
480
|
+
expect(sizeButton).to.exist;
|
|
481
|
+
sizeButton.click();
|
|
482
|
+
await simulator.updateComplete;
|
|
483
|
+
// should now be large (medium -> large)
|
|
484
|
+
expect(simulator.size).to.equal('large');
|
|
485
|
+
});
|
|
486
|
+
it('resets simulation', async () => {
|
|
487
|
+
mockSimulatorStart();
|
|
488
|
+
const simulator = await createSimulator();
|
|
489
|
+
simulator.size = 'medium';
|
|
490
|
+
await simulator.updateComplete;
|
|
491
|
+
await openSimulator(simulator);
|
|
492
|
+
// send a message first
|
|
493
|
+
mockSimulatorResume('Response to test message');
|
|
494
|
+
const input = simulator.shadowRoot.querySelector('.message-input input');
|
|
495
|
+
input.value = 'Test message';
|
|
496
|
+
input.dispatchEvent(new Event('input'));
|
|
497
|
+
const enterEvent = new KeyboardEvent('keyup', {
|
|
498
|
+
key: 'Enter',
|
|
499
|
+
bubbles: true
|
|
500
|
+
});
|
|
501
|
+
input.dispatchEvent(enterEvent);
|
|
502
|
+
await delay(1000);
|
|
503
|
+
await simulator.updateComplete;
|
|
504
|
+
// verify we have multiple messages
|
|
505
|
+
let messages = simulator.shadowRoot.querySelectorAll('.message');
|
|
506
|
+
const messageCountBefore = messages.length;
|
|
507
|
+
expect(messageCountBefore).to.be.greaterThan(1);
|
|
508
|
+
// mock the start response for reset
|
|
509
|
+
mockSimulatorStart();
|
|
510
|
+
// click the reset button (has delete icon)
|
|
511
|
+
const optionButtons = Array.from(simulator.shadowRoot.querySelectorAll('.option-btn'));
|
|
512
|
+
const resetButton = optionButtons.find((btn) => btn.querySelector('temba-icon[name="delete"]'));
|
|
513
|
+
expect(resetButton).to.exist;
|
|
514
|
+
resetButton.click();
|
|
515
|
+
await delay(100);
|
|
516
|
+
await simulator.updateComplete;
|
|
517
|
+
// verify messages are reset - should go back to just initial message
|
|
518
|
+
messages = simulator.shadowRoot.querySelectorAll('.message');
|
|
519
|
+
expect(messages.length).to.be.lessThan(messageCountBefore);
|
|
520
|
+
await assertScreenshot('simulator/after-reset', getSimulatorClip(simulator));
|
|
521
|
+
});
|
|
522
|
+
it('displays event info messages', async () => {
|
|
523
|
+
const responseWithEvents = {
|
|
524
|
+
session: {
|
|
525
|
+
status: 'waiting',
|
|
526
|
+
trigger: {
|
|
527
|
+
type: 'manual',
|
|
528
|
+
flow: { uuid: FLOW_UUID, name: 'Test Flow' }
|
|
529
|
+
},
|
|
530
|
+
runs: [
|
|
531
|
+
{
|
|
532
|
+
uuid: 'run-1',
|
|
533
|
+
flow: { uuid: FLOW_UUID, name: 'Test Flow' },
|
|
534
|
+
status: 'waiting',
|
|
535
|
+
path: [
|
|
536
|
+
{
|
|
537
|
+
uuid: 'step-1',
|
|
538
|
+
node_uuid: 'node-1',
|
|
539
|
+
arrived_on: new Date().toISOString(),
|
|
540
|
+
exit_uuid: null
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
}
|
|
544
|
+
],
|
|
545
|
+
environment: {
|
|
546
|
+
date_format: 'YYYY-MM-DD',
|
|
547
|
+
time_format: 'HH:mm',
|
|
548
|
+
timezone: 'America/New_York',
|
|
549
|
+
allowed_languages: ['eng'],
|
|
550
|
+
default_country: 'US'
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
events: [
|
|
554
|
+
{
|
|
555
|
+
type: 'contact_field_changed',
|
|
556
|
+
created_on: new Date().toISOString(),
|
|
557
|
+
field: { key: 'name', name: 'Name' },
|
|
558
|
+
value: { text: 'John Doe' }
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
type: 'msg_created',
|
|
562
|
+
created_on: new Date().toISOString(),
|
|
563
|
+
msg: {
|
|
564
|
+
uuid: 'msg-1',
|
|
565
|
+
text: 'Your name has been updated!',
|
|
566
|
+
urn: 'tel:+12065551212'
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
contact: {
|
|
571
|
+
uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
|
|
572
|
+
urns: ['tel:+12065551212'],
|
|
573
|
+
fields: {
|
|
574
|
+
name: 'John Doe'
|
|
575
|
+
},
|
|
576
|
+
groups: [],
|
|
577
|
+
language: 'eng',
|
|
578
|
+
status: 'active',
|
|
579
|
+
created_on: new Date().toISOString()
|
|
580
|
+
},
|
|
581
|
+
context: {
|
|
582
|
+
contact: {
|
|
583
|
+
uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
|
|
584
|
+
name: 'John Doe',
|
|
585
|
+
fields: {
|
|
586
|
+
name: 'John Doe'
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
mockPOST(/\/flow\/simulate\/.*\//, responseWithEvents);
|
|
592
|
+
const simulator = await createSimulator();
|
|
593
|
+
simulator.size = 'medium';
|
|
594
|
+
await simulator.updateComplete;
|
|
595
|
+
await openSimulator(simulator);
|
|
596
|
+
// verify event info is displayed
|
|
597
|
+
const eventInfo = simulator.shadowRoot.querySelectorAll('.event-info');
|
|
598
|
+
expect(eventInfo.length).to.be.greaterThan(0);
|
|
599
|
+
await assertScreenshot('simulator/event-info', getSimulatorClip(simulator));
|
|
600
|
+
});
|
|
601
|
+
it('displays different simulator sizes', async () => {
|
|
602
|
+
mockSimulatorStart();
|
|
603
|
+
const simulator = await createSimulator();
|
|
604
|
+
await openSimulator(simulator);
|
|
605
|
+
// get size button - find it by checking if textContent is a size indicator
|
|
606
|
+
let optionButtons = Array.from(simulator.shadowRoot.querySelectorAll('.option-btn'));
|
|
607
|
+
let sizeButton = optionButtons.find((btn) => {
|
|
608
|
+
var _a;
|
|
609
|
+
const text = (_a = btn.textContent) === null || _a === void 0 ? void 0 : _a.trim();
|
|
610
|
+
return text === 'S' || text === 'M' || text === 'L';
|
|
611
|
+
});
|
|
612
|
+
expect(sizeButton).to.exist;
|
|
613
|
+
// cycle to next size
|
|
614
|
+
sizeButton.click();
|
|
615
|
+
await simulator.updateComplete;
|
|
616
|
+
await delay(200);
|
|
617
|
+
// verify size changed
|
|
618
|
+
expect(simulator.size).to.equal('large');
|
|
619
|
+
// re-query the button after it updated
|
|
620
|
+
optionButtons = Array.from(simulator.shadowRoot.querySelectorAll('.option-btn'));
|
|
621
|
+
sizeButton = optionButtons.find((btn) => {
|
|
622
|
+
var _a;
|
|
623
|
+
const text = (_a = btn.textContent) === null || _a === void 0 ? void 0 : _a.trim();
|
|
624
|
+
return text === 'S' || text === 'M' || text === 'L';
|
|
625
|
+
});
|
|
626
|
+
// cycle to next size
|
|
627
|
+
sizeButton.click();
|
|
628
|
+
await simulator.updateComplete;
|
|
629
|
+
await delay(200);
|
|
630
|
+
expect(simulator.size).to.equal('small');
|
|
631
|
+
});
|
|
632
|
+
it('verifies simulator endpoint configuration', async () => {
|
|
633
|
+
const simulator = await createSimulator();
|
|
634
|
+
// verify endpoint is set correctly from flow prop
|
|
635
|
+
expect(simulator.endpoint).to.equal(`/flow/simulate/${FLOW_UUID}/`);
|
|
636
|
+
// change flow prop and verify endpoint updates
|
|
637
|
+
simulator.flow = 'different-flow-456';
|
|
638
|
+
await simulator.updateComplete;
|
|
639
|
+
expect(simulator.endpoint).to.equal('/flow/simulate/different-flow-456/');
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
//# sourceMappingURL=temba-simulator.test.js.map
|