@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/demo/components/webchat/example.html +4 -2
  3. package/dist/static/svg/index.svg +1 -1
  4. package/dist/temba-components.js +1323 -317
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/Icons.js +2 -1
  7. package/out-tsc/src/Icons.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +11 -0
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +224 -2
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/Plumber.js +320 -1
  13. package/out-tsc/src/flow/Plumber.js.map +1 -1
  14. package/out-tsc/src/interfaces.js +1 -0
  15. package/out-tsc/src/interfaces.js.map +1 -1
  16. package/out-tsc/src/layout/FloatingWindow.js +30 -8
  17. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  18. package/out-tsc/src/simulator/Simulator.js +1827 -0
  19. package/out-tsc/src/simulator/Simulator.js.map +1 -0
  20. package/out-tsc/src/store/AppState.js +33 -0
  21. package/out-tsc/src/store/AppState.js.map +1 -1
  22. package/out-tsc/src/utils.js +48 -0
  23. package/out-tsc/src/utils.js.map +1 -1
  24. package/out-tsc/temba-modules.js +2 -0
  25. package/out-tsc/temba-modules.js.map +1 -1
  26. package/out-tsc/test/temba-flow-editor.test.js +1 -1
  27. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  28. package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
  29. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  30. package/out-tsc/test/temba-flow-plumber.test.js +3 -1
  31. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  32. package/out-tsc/test/temba-simulator.test.js +642 -0
  33. package/out-tsc/test/temba-simulator.test.js.map +1 -0
  34. package/out-tsc/test/utils.test.js +1 -1
  35. package/out-tsc/test/utils.test.js.map +1 -1
  36. package/package.json +1 -1
  37. package/screenshots/truth/simulator/after-message-sent.png +0 -0
  38. package/screenshots/truth/simulator/after-reset.png +0 -0
  39. package/screenshots/truth/simulator/attachment-menu.png +0 -0
  40. package/screenshots/truth/simulator/context-expanded.png +0 -0
  41. package/screenshots/truth/simulator/context-explorer-open.png +0 -0
  42. package/screenshots/truth/simulator/event-info.png +0 -0
  43. package/screenshots/truth/simulator/image-attachment.png +0 -0
  44. package/screenshots/truth/simulator/open-initial.png +0 -0
  45. package/screenshots/truth/simulator/quick-replies.png +0 -0
  46. package/src/Icons.ts +2 -1
  47. package/src/flow/CanvasNode.ts +12 -0
  48. package/src/flow/Editor.ts +240 -1
  49. package/src/flow/Plumber.ts +371 -2
  50. package/src/interfaces.ts +2 -1
  51. package/src/layout/FloatingWindow.ts +36 -11
  52. package/src/simulator/Simulator.ts +2008 -0
  53. package/src/store/AppState.ts +53 -0
  54. package/src/utils.ts +53 -0
  55. package/static/svg/index.svg +1 -1
  56. package/static/svg/work/traced/route.svg +1 -0
  57. package/static/svg/work/used/route.svg +3 -0
  58. package/temba-modules.ts +2 -0
  59. package/test/temba-flow-editor.test.ts +1 -1
  60. package/test/temba-flow-plumber-connections.test.ts +4 -1
  61. package/test/temba-flow-plumber.test.ts +4 -1
  62. package/test/temba-simulator.test.ts +866 -0
  63. 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