@nyaruka/temba-components 0.126.0 → 0.128.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 +23 -0
- package/demo/chart/example.html +18 -1
- package/demo/data/flows/sample-flow.json +127 -100
- package/demo/data/server/opened-tickets-long.json +53 -0
- package/demo/sticky-note-demo.html +152 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +11 -2
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +346 -86
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +44 -5
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +210 -1
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/EditorNode.js +98 -142
- package/out-tsc/src/flow/EditorNode.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +272 -0
- package/out-tsc/src/flow/StickyNote.js.map +1 -0
- package/out-tsc/src/list/RunList.js +2 -1
- package/out-tsc/src/list/RunList.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +9 -0
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +11 -2
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/store/AppState.js +33 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/vectoricon/index.js +2 -1
- package/out-tsc/src/vectoricon/index.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-node.test.js +249 -5
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +9 -14
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +62 -0
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/sticky-note/blue.png +0 -0
- package/screenshots/truth/sticky-note/gray.png +0 -0
- package/screenshots/truth/sticky-note/green.png +0 -0
- package/screenshots/truth/sticky-note/pink.png +0 -0
- package/screenshots/truth/sticky-note/yellow.png +0 -0
- package/src/chart/TembaChart.ts +47 -5
- package/src/flow/Editor.ts +252 -2
- package/src/flow/EditorNode.ts +98 -160
- package/src/flow/StickyNote.ts +284 -0
- package/src/list/RunList.ts +2 -1
- package/src/list/SortableList.ts +11 -0
- package/src/locales/es.ts +18 -13
- package/src/locales/fr.ts +18 -13
- package/src/locales/locale-codes.ts +11 -2
- package/src/locales/pt.ts +18 -13
- package/src/store/AppState.ts +51 -1
- package/src/store/flow-definition.d.ts +8 -0
- package/src/vectoricon/index.ts +2 -1
- package/static/svg/index.pdf +137 -0
- package/temba-modules.ts +2 -0
- package/test/temba-flow-editor-node.test.ts +322 -6
- package/test/temba-select.test.ts +10 -17
- package/test/utils.test.ts +98 -0
- package/web-dev-server.config.mjs +30 -22
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import '../temba-modules';
|
|
1
2
|
import { html, fixture, expect } from '@open-wc/testing';
|
|
2
3
|
import { EditorNode } from '../src/flow/EditorNode';
|
|
3
4
|
import {
|
|
@@ -8,9 +9,7 @@ import {
|
|
|
8
9
|
Router
|
|
9
10
|
} from '../src/store/flow-definition.d';
|
|
10
11
|
import { stub, restore } from 'sinon';
|
|
11
|
-
|
|
12
|
-
// Register the component
|
|
13
|
-
customElements.define('temba-editor-node', EditorNode);
|
|
12
|
+
import { CustomEventType } from '../src/interfaces';
|
|
14
13
|
|
|
15
14
|
describe('EditorNode', () => {
|
|
16
15
|
let editorNode: EditorNode;
|
|
@@ -55,7 +54,7 @@ describe('EditorNode', () => {
|
|
|
55
54
|
quick_replies: []
|
|
56
55
|
};
|
|
57
56
|
|
|
58
|
-
const result = (editorNode as any).renderAction(mockNode, action);
|
|
57
|
+
const result = (editorNode as any).renderAction(mockNode, action, 0);
|
|
59
58
|
expect(result).to.exist;
|
|
60
59
|
});
|
|
61
60
|
|
|
@@ -71,7 +70,7 @@ describe('EditorNode', () => {
|
|
|
71
70
|
uuid: 'action-1'
|
|
72
71
|
};
|
|
73
72
|
|
|
74
|
-
const result = (editorNode as any).renderAction(mockNode, action);
|
|
73
|
+
const result = (editorNode as any).renderAction(mockNode, action, 1);
|
|
75
74
|
expect(result).to.exist;
|
|
76
75
|
});
|
|
77
76
|
});
|
|
@@ -327,7 +326,8 @@ describe('EditorNode', () => {
|
|
|
327
326
|
// Test renderAction
|
|
328
327
|
const actionResult = (editorNode as any).renderAction(
|
|
329
328
|
mockNode,
|
|
330
|
-
mockNode.actions[0]
|
|
329
|
+
mockNode.actions[0],
|
|
330
|
+
0
|
|
331
331
|
);
|
|
332
332
|
expect(actionResult).to.exist;
|
|
333
333
|
|
|
@@ -341,4 +341,320 @@ describe('EditorNode', () => {
|
|
|
341
341
|
expect(mockNode.exits).to.have.length(1);
|
|
342
342
|
});
|
|
343
343
|
});
|
|
344
|
+
|
|
345
|
+
describe('drag and drop functionality', () => {
|
|
346
|
+
let editorNode: EditorNode;
|
|
347
|
+
|
|
348
|
+
beforeEach(() => {
|
|
349
|
+
editorNode = new EditorNode();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('renders actions with sortable class and proper IDs', async () => {
|
|
353
|
+
const mockNode: Node = {
|
|
354
|
+
uuid: 'sortable-test-node',
|
|
355
|
+
actions: [
|
|
356
|
+
{
|
|
357
|
+
type: 'send_msg',
|
|
358
|
+
uuid: 'action-1',
|
|
359
|
+
text: 'Hello',
|
|
360
|
+
quick_replies: []
|
|
361
|
+
} as any,
|
|
362
|
+
{
|
|
363
|
+
type: 'send_msg',
|
|
364
|
+
uuid: 'action-2',
|
|
365
|
+
text: 'World',
|
|
366
|
+
quick_replies: []
|
|
367
|
+
} as any
|
|
368
|
+
],
|
|
369
|
+
exits: []
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Test that renderAction includes sortable class and proper ID
|
|
373
|
+
const result1 = (editorNode as any).renderAction(
|
|
374
|
+
mockNode,
|
|
375
|
+
mockNode.actions[0],
|
|
376
|
+
0
|
|
377
|
+
);
|
|
378
|
+
const result2 = (editorNode as any).renderAction(
|
|
379
|
+
mockNode,
|
|
380
|
+
mockNode.actions[1],
|
|
381
|
+
1
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
expect(result1).to.exist;
|
|
385
|
+
expect(result2).to.exist;
|
|
386
|
+
|
|
387
|
+
// Render the template to check the actual DOM
|
|
388
|
+
const container1 = await fixture(html`<div>${result1}</div>`);
|
|
389
|
+
const container2 = await fixture(html`<div>${result2}</div>`);
|
|
390
|
+
|
|
391
|
+
const actionElement1 = container1.querySelector('.action');
|
|
392
|
+
const actionElement2 = container2.querySelector('.action');
|
|
393
|
+
|
|
394
|
+
expect(actionElement1).to.exist;
|
|
395
|
+
expect(actionElement1?.classList.contains('sortable')).to.be.true;
|
|
396
|
+
expect(actionElement1?.id).to.equal('action-0');
|
|
397
|
+
|
|
398
|
+
expect(actionElement2).to.exist;
|
|
399
|
+
expect(actionElement2?.classList.contains('sortable')).to.be.true;
|
|
400
|
+
expect(actionElement2?.id).to.equal('action-1');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('includes drag handle in rendered actions', async () => {
|
|
404
|
+
const mockNode: Node = {
|
|
405
|
+
uuid: 'drag-handle-test',
|
|
406
|
+
actions: [
|
|
407
|
+
{
|
|
408
|
+
type: 'send_msg',
|
|
409
|
+
uuid: 'action-1',
|
|
410
|
+
text: 'Hello',
|
|
411
|
+
quick_replies: []
|
|
412
|
+
} as any
|
|
413
|
+
],
|
|
414
|
+
exits: []
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
let editorNode: EditorNode = await fixture(
|
|
418
|
+
html`<temba-flow-node
|
|
419
|
+
.node=${mockNode}
|
|
420
|
+
.ui=${{ position: { left: 0, top: 0 } }}
|
|
421
|
+
></temba-flow-node>`
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// No drag handle should be present if only one action
|
|
425
|
+
let dragHandle = editorNode.querySelector('.drag-handle');
|
|
426
|
+
expect(dragHandle).to.not.exist;
|
|
427
|
+
|
|
428
|
+
// Now add a second action to verify drag handle appears
|
|
429
|
+
mockNode.actions.push({
|
|
430
|
+
type: 'send_msg',
|
|
431
|
+
uuid: 'action-2',
|
|
432
|
+
text: 'World',
|
|
433
|
+
quick_replies: []
|
|
434
|
+
} as any);
|
|
435
|
+
|
|
436
|
+
editorNode = await fixture(
|
|
437
|
+
html`<temba-flow-node
|
|
438
|
+
.node=${mockNode}
|
|
439
|
+
.ui=${{ position: { left: 0, top: 0 } }}
|
|
440
|
+
></temba-flow-node>`
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
dragHandle = editorNode.querySelector('.drag-handle');
|
|
444
|
+
expect(dragHandle).to.exist;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('renders SortableList when actions are present', async () => {
|
|
448
|
+
const mockNode: Node = {
|
|
449
|
+
uuid: 'sortable-list-test',
|
|
450
|
+
actions: [
|
|
451
|
+
{
|
|
452
|
+
type: 'send_msg',
|
|
453
|
+
uuid: 'action-1',
|
|
454
|
+
text: 'Hello',
|
|
455
|
+
quick_replies: []
|
|
456
|
+
} as any,
|
|
457
|
+
{
|
|
458
|
+
type: 'send_msg',
|
|
459
|
+
uuid: 'action-2',
|
|
460
|
+
text: 'World',
|
|
461
|
+
quick_replies: []
|
|
462
|
+
} as any
|
|
463
|
+
],
|
|
464
|
+
exits: []
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const mockUI: NodeUI = {
|
|
468
|
+
position: { left: 100, top: 200 },
|
|
469
|
+
type: 'execute_actions'
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// Set properties on the component
|
|
473
|
+
(editorNode as any).node = mockNode;
|
|
474
|
+
(editorNode as any).ui = mockUI;
|
|
475
|
+
|
|
476
|
+
const renderResult = editorNode.render();
|
|
477
|
+
|
|
478
|
+
// Render the template to check the actual DOM
|
|
479
|
+
const container = await fixture(html`<div>${renderResult}</div>`);
|
|
480
|
+
|
|
481
|
+
const sortableList = container.querySelector('temba-sortable-list');
|
|
482
|
+
expect(sortableList).to.exist;
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('does not render SortableList when no actions', async () => {
|
|
486
|
+
const mockNode: Node = {
|
|
487
|
+
uuid: 'no-actions-test',
|
|
488
|
+
actions: [],
|
|
489
|
+
exits: [{ uuid: 'exit-1' }]
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const mockUI: NodeUI = {
|
|
493
|
+
position: { left: 100, top: 200 },
|
|
494
|
+
type: 'execute_actions'
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Set properties on the component
|
|
498
|
+
(editorNode as any).node = mockNode;
|
|
499
|
+
(editorNode as any).ui = mockUI;
|
|
500
|
+
|
|
501
|
+
const renderResult = editorNode.render();
|
|
502
|
+
|
|
503
|
+
// Check that template does not include temba-sortable-list
|
|
504
|
+
expect(renderResult.strings.join('')).to.not.contain(
|
|
505
|
+
'temba-sortable-list'
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('handles order changed events correctly', async () => {
|
|
510
|
+
const mockNode: Node = {
|
|
511
|
+
uuid: 'order-test',
|
|
512
|
+
actions: [
|
|
513
|
+
{
|
|
514
|
+
type: 'send_msg',
|
|
515
|
+
uuid: 'action-1',
|
|
516
|
+
text: 'First',
|
|
517
|
+
quick_replies: []
|
|
518
|
+
} as any,
|
|
519
|
+
{
|
|
520
|
+
type: 'send_msg',
|
|
521
|
+
uuid: 'action-2',
|
|
522
|
+
text: 'Second',
|
|
523
|
+
quick_replies: []
|
|
524
|
+
} as any,
|
|
525
|
+
{
|
|
526
|
+
type: 'send_msg',
|
|
527
|
+
uuid: 'action-3',
|
|
528
|
+
text: 'Third',
|
|
529
|
+
quick_replies: []
|
|
530
|
+
} as any
|
|
531
|
+
],
|
|
532
|
+
exits: []
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
(editorNode as any).node = mockNode;
|
|
536
|
+
|
|
537
|
+
// Create a mock order changed event (swap first and last actions)
|
|
538
|
+
const orderChangedEvent = new CustomEvent(CustomEventType.OrderChanged, {
|
|
539
|
+
detail: { swap: [0, 2] }
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Call the handler directly
|
|
543
|
+
(editorNode as any).handleActionOrderChanged(orderChangedEvent);
|
|
544
|
+
|
|
545
|
+
// Verify the actions were reordered correctly
|
|
546
|
+
expect((editorNode as any).node.actions).to.have.length(3);
|
|
547
|
+
expect(((editorNode as any).node.actions[0] as any).text).to.equal(
|
|
548
|
+
'Second'
|
|
549
|
+
);
|
|
550
|
+
expect(((editorNode as any).node.actions[1] as any).text).to.equal(
|
|
551
|
+
'Third'
|
|
552
|
+
);
|
|
553
|
+
expect(((editorNode as any).node.actions[2] as any).text).to.equal(
|
|
554
|
+
'First'
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('preserves action data during reordering', () => {
|
|
559
|
+
const mockNode: Node = {
|
|
560
|
+
uuid: 'preserve-test',
|
|
561
|
+
actions: [
|
|
562
|
+
{
|
|
563
|
+
type: 'send_msg',
|
|
564
|
+
uuid: 'action-1',
|
|
565
|
+
text: 'Message 1',
|
|
566
|
+
quick_replies: ['Yes', 'No']
|
|
567
|
+
} as any,
|
|
568
|
+
{
|
|
569
|
+
type: 'send_msg',
|
|
570
|
+
uuid: 'action-2',
|
|
571
|
+
text: 'Message 2',
|
|
572
|
+
quick_replies: []
|
|
573
|
+
} as any
|
|
574
|
+
],
|
|
575
|
+
exits: []
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
(editorNode as any).node = mockNode;
|
|
579
|
+
|
|
580
|
+
// Swap the two actions
|
|
581
|
+
const orderChangedEvent = new CustomEvent(CustomEventType.OrderChanged, {
|
|
582
|
+
detail: { swap: [0, 1] }
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
(editorNode as any).handleActionOrderChanged(orderChangedEvent);
|
|
586
|
+
|
|
587
|
+
// Verify all action data is preserved
|
|
588
|
+
expect((editorNode as any).node.actions).to.have.length(2);
|
|
589
|
+
expect(((editorNode as any).node.actions[0] as any).text).to.equal(
|
|
590
|
+
'Message 2'
|
|
591
|
+
);
|
|
592
|
+
expect(
|
|
593
|
+
((editorNode as any).node.actions[0] as any).quick_replies
|
|
594
|
+
).to.deep.equal([]);
|
|
595
|
+
expect(((editorNode as any).node.actions[1] as any).text).to.equal(
|
|
596
|
+
'Message 1'
|
|
597
|
+
);
|
|
598
|
+
expect(
|
|
599
|
+
((editorNode as any).node.actions[1] as any).quick_replies
|
|
600
|
+
).to.deep.equal(['Yes', 'No']);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('integrates with SortableList for full drag functionality', async () => {
|
|
604
|
+
const mockNode: Node = {
|
|
605
|
+
uuid: 'integration-drag-test',
|
|
606
|
+
actions: [
|
|
607
|
+
{
|
|
608
|
+
type: 'send_msg',
|
|
609
|
+
uuid: 'action-1',
|
|
610
|
+
text: 'First Action',
|
|
611
|
+
quick_replies: []
|
|
612
|
+
} as any,
|
|
613
|
+
{
|
|
614
|
+
type: 'send_msg',
|
|
615
|
+
uuid: 'action-2',
|
|
616
|
+
text: 'Second Action',
|
|
617
|
+
quick_replies: []
|
|
618
|
+
} as any,
|
|
619
|
+
{
|
|
620
|
+
type: 'send_msg',
|
|
621
|
+
uuid: 'action-3',
|
|
622
|
+
text: 'Third Action',
|
|
623
|
+
quick_replies: []
|
|
624
|
+
} as any
|
|
625
|
+
],
|
|
626
|
+
exits: []
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const mockUI: NodeUI = {
|
|
630
|
+
position: { left: 100, top: 200 },
|
|
631
|
+
type: 'execute_actions'
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// Set properties on the component
|
|
635
|
+
(editorNode as any).node = mockNode;
|
|
636
|
+
(editorNode as any).ui = mockUI;
|
|
637
|
+
|
|
638
|
+
// Render the full component
|
|
639
|
+
const renderResult = editorNode.render();
|
|
640
|
+
const container = await fixture(html`<div>${renderResult}</div>`);
|
|
641
|
+
|
|
642
|
+
// Find the sortable list
|
|
643
|
+
const sortableList = container.querySelector('temba-sortable-list');
|
|
644
|
+
expect(sortableList).to.exist;
|
|
645
|
+
|
|
646
|
+
// Verify all actions are rendered as sortable items
|
|
647
|
+
const sortableItems = container.querySelectorAll('.sortable');
|
|
648
|
+
expect(sortableItems).to.have.length(3);
|
|
649
|
+
|
|
650
|
+
// Verify each action has correct ID and structure
|
|
651
|
+
expect(sortableItems[0].id).to.equal('action-0');
|
|
652
|
+
expect(sortableItems[1].id).to.equal('action-1');
|
|
653
|
+
expect(sortableItems[2].id).to.equal('action-2');
|
|
654
|
+
|
|
655
|
+
// Verify drag handles are present
|
|
656
|
+
const dragHandles = container.querySelectorAll('.drag-handle');
|
|
657
|
+
expect(dragHandles).to.have.length(3);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
344
660
|
});
|
|
@@ -5,12 +5,12 @@ import { Options } from '../src/options/Options';
|
|
|
5
5
|
import { Select, SelectOption } from '../src/select/Select';
|
|
6
6
|
import {
|
|
7
7
|
assertScreenshot,
|
|
8
|
-
delay,
|
|
9
8
|
getClip,
|
|
10
9
|
getOptions,
|
|
11
10
|
loadStore,
|
|
12
11
|
openAndClick,
|
|
13
|
-
openSelect
|
|
12
|
+
openSelect,
|
|
13
|
+
waitForSelectPagination
|
|
14
14
|
} from './utils.test';
|
|
15
15
|
|
|
16
16
|
const colors = [
|
|
@@ -661,21 +661,9 @@ describe('temba-select', () => {
|
|
|
661
661
|
|
|
662
662
|
await openSelect(clock, select);
|
|
663
663
|
|
|
664
|
-
// Wait for pagination to complete
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
const maxAttempts = 10;
|
|
668
|
-
while (select.fetching || select.visibleOptions.length < 15) {
|
|
669
|
-
if (attempts >= maxAttempts) {
|
|
670
|
-
throw new Error(
|
|
671
|
-
`Pagination did not complete after ${maxAttempts} attempts. fetching: ${select.fetching}, visibleOptions: ${select.visibleOptions.length}`
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
await select.updateComplete;
|
|
675
|
-
clock.runAll();
|
|
676
|
-
attempts++;
|
|
677
|
-
await delay(100);
|
|
678
|
-
}
|
|
664
|
+
// Wait for pagination to complete using our improved helper
|
|
665
|
+
// Use more attempts for this test since pagination can be slow in CI
|
|
666
|
+
await waitForSelectPagination(select, clock, 15, 50);
|
|
679
667
|
|
|
680
668
|
// should have all three pages visible right away
|
|
681
669
|
assert.equal(select.visibleOptions.length, 15);
|
|
@@ -694,13 +682,18 @@ describe('temba-select', () => {
|
|
|
694
682
|
|
|
695
683
|
// wait for updates from fetching three pages
|
|
696
684
|
await openSelect(clock, select);
|
|
685
|
+
await waitForSelectPagination(select, clock, 15, 50);
|
|
697
686
|
assert.equal(select.visibleOptions.length, 15);
|
|
698
687
|
|
|
699
688
|
// close and reopen
|
|
700
689
|
select.blur();
|
|
701
690
|
await clock.tick(250);
|
|
691
|
+
// Ensure the select is properly closed before reopening
|
|
692
|
+
await select.updateComplete;
|
|
702
693
|
|
|
703
694
|
await openSelect(clock, select);
|
|
695
|
+
// Cached results should be available immediately, but give some time for rendering
|
|
696
|
+
await waitForSelectPagination(select, clock, 15, 30);
|
|
704
697
|
assert.equal(select.visibleOptions.length, 15);
|
|
705
698
|
|
|
706
699
|
// close and reopen once more (previous bug failed on third opening)
|
package/test/utils.test.ts
CHANGED
|
@@ -180,6 +180,26 @@ export const delay = (millis: number) => {
|
|
|
180
180
|
});
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
// Enhanced wait utility for more robust testing
|
|
184
|
+
export const waitForCondition = async (
|
|
185
|
+
predicate: () => boolean,
|
|
186
|
+
maxAttempts: number = 20,
|
|
187
|
+
delayMs: number = 50
|
|
188
|
+
): Promise<void> => {
|
|
189
|
+
let attempts = 0;
|
|
190
|
+
while (!predicate() && attempts < maxAttempts) {
|
|
191
|
+
await delay(delayMs);
|
|
192
|
+
attempts++;
|
|
193
|
+
}
|
|
194
|
+
if (!predicate()) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Condition not met after ${maxAttempts} attempts (${
|
|
197
|
+
maxAttempts * delayMs
|
|
198
|
+
}ms)`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
183
203
|
export const assertScreenshot = async (
|
|
184
204
|
filename: string,
|
|
185
205
|
clip: Clip,
|
|
@@ -294,6 +314,31 @@ export const clickOption = async (
|
|
|
294
314
|
index: number
|
|
295
315
|
) => {
|
|
296
316
|
const options = getOptions(select);
|
|
317
|
+
if (!options) {
|
|
318
|
+
throw new Error('No options element found');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Wait for the specific option to be available, but only if it's not already there
|
|
322
|
+
const existingOption = options.shadowRoot?.querySelector(
|
|
323
|
+
`[data-option-index="${index}"]`
|
|
324
|
+
);
|
|
325
|
+
if (!existingOption) {
|
|
326
|
+
try {
|
|
327
|
+
await waitForCondition(
|
|
328
|
+
() => {
|
|
329
|
+
const option = options.shadowRoot?.querySelector(
|
|
330
|
+
`[data-option-index="${index}"]`
|
|
331
|
+
);
|
|
332
|
+
return !!option;
|
|
333
|
+
},
|
|
334
|
+
10,
|
|
335
|
+
25
|
|
336
|
+
);
|
|
337
|
+
} catch (e) {
|
|
338
|
+
throw new Error(`Option at index ${index} not found after waiting`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
297
342
|
const option = options.shadowRoot.querySelector(
|
|
298
343
|
`[data-option-index="${index}"]`
|
|
299
344
|
) as HTMLDivElement;
|
|
@@ -318,6 +363,27 @@ export const openSelect = async (clock: any, select: Select<SelectOption>) => {
|
|
|
318
363
|
// reduce wait time for options to become visible
|
|
319
364
|
await waitFor(25);
|
|
320
365
|
clock.runAll();
|
|
366
|
+
|
|
367
|
+
// For non-endpoint selects, options might be immediately available
|
|
368
|
+
// For endpoint selects, we need to wait for them to load
|
|
369
|
+
const hasEndpoint = select.getAttribute('endpoint');
|
|
370
|
+
if (hasEndpoint) {
|
|
371
|
+
try {
|
|
372
|
+
// Wait for options to be properly rendered and visible (but only for endpoint selects)
|
|
373
|
+
await waitForCondition(
|
|
374
|
+
() => {
|
|
375
|
+
const options = select.shadowRoot.querySelector(
|
|
376
|
+
'temba-options[visible]'
|
|
377
|
+
);
|
|
378
|
+
return options && options.isConnected;
|
|
379
|
+
},
|
|
380
|
+
10,
|
|
381
|
+
25
|
|
382
|
+
);
|
|
383
|
+
} catch (e) {
|
|
384
|
+
// If condition fails, continue - some tests might not need options to be visible immediately
|
|
385
|
+
}
|
|
386
|
+
}
|
|
321
387
|
};
|
|
322
388
|
|
|
323
389
|
export const openAndClick = async (
|
|
@@ -367,3 +433,35 @@ export const updateComponent = async (
|
|
|
367
433
|
export const getValidText = () => {
|
|
368
434
|
return 'sà-wàd-dee!';
|
|
369
435
|
};
|
|
436
|
+
|
|
437
|
+
// Helper for waiting for select pagination to complete
|
|
438
|
+
export const waitForSelectPagination = async (
|
|
439
|
+
select: Select<SelectOption>,
|
|
440
|
+
clock: any,
|
|
441
|
+
expectedCount: number,
|
|
442
|
+
maxAttempts: number = 30
|
|
443
|
+
): Promise<void> => {
|
|
444
|
+
let attempts = 0;
|
|
445
|
+
while (attempts < maxAttempts) {
|
|
446
|
+
// Ensure we're not still fetching
|
|
447
|
+
if (!select.fetching && select.visibleOptions.length >= expectedCount) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
await select.updateComplete;
|
|
452
|
+
clock.runAll();
|
|
453
|
+
|
|
454
|
+
// Give more time between attempts for slow CI environments
|
|
455
|
+
await delay(75);
|
|
456
|
+
|
|
457
|
+
attempts++;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Pagination did not complete after ${maxAttempts} attempts (${
|
|
462
|
+
maxAttempts * 75
|
|
463
|
+
}ms). ` +
|
|
464
|
+
`Expected ${expectedCount} options, got ${select.visibleOptions.length}. ` +
|
|
465
|
+
`Fetching: ${select.fetching}`
|
|
466
|
+
);
|
|
467
|
+
};
|
|
@@ -15,30 +15,38 @@ export default {
|
|
|
15
15
|
{
|
|
16
16
|
name: 'flow-files',
|
|
17
17
|
serve(context) {
|
|
18
|
-
if (context.request.method === 'POST') {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
body += chunk.toString();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
context.req.on('end', () => {
|
|
18
|
+
if (context.request.method === 'POST' && context.path.startsWith('/flow/revisions/')) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
let body = '';
|
|
25
21
|
const parts = context.path.split('/');
|
|
26
22
|
const uuid = parts[3];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
23
|
+
context.req.on('data', chunk => {
|
|
24
|
+
body += chunk.toString();
|
|
25
|
+
});
|
|
26
|
+
context.req.on('end', () => {
|
|
27
|
+
context.contentType = 'application/json';
|
|
28
|
+
if (body) {
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
path.resolve(`./demo/data/flows/${uuid}.json`),
|
|
31
|
+
JSON.stringify({ definition: JSON.parse(body) }, null, 2)
|
|
32
|
+
);
|
|
33
|
+
console.log(`Flow ${uuid} saved successfully.`);
|
|
34
|
+
context.body = {
|
|
35
|
+
status: 'success',
|
|
36
|
+
message: `Flow ${uuid} saved successfully.`,
|
|
37
|
+
definition: JSON.parse(body),
|
|
38
|
+
};
|
|
39
|
+
context.status = 200;
|
|
40
|
+
} else {
|
|
41
|
+
console.log(`No body received for flow ${uuid}.`);
|
|
42
|
+
context.body = {
|
|
43
|
+
status: 'error',
|
|
44
|
+
message: `No body received for flow ${uuid}.`,
|
|
45
|
+
};
|
|
46
|
+
context.status = 400;
|
|
47
|
+
}
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
42
50
|
});
|
|
43
51
|
}
|
|
44
52
|
|