@nyaruka/temba-components 0.127.0 → 0.129.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 (109) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/demo/chart/horizontal-demo.html +81 -0
  3. package/demo/components/datepicker/example.html +63 -0
  4. package/demo/components/datepicker/range-picker-demo.html +161 -0
  5. package/demo/data/flows/sample-flow.json +127 -100
  6. package/demo/index.html +8 -0
  7. package/demo/static/css/prism.css +2 -0
  8. package/demo/static/js/prism-loader.js +12 -0
  9. package/demo/sticky-note-demo.html +152 -0
  10. package/demo/styles.css +71 -1
  11. package/dist/locales/es.js +5 -5
  12. package/dist/locales/es.js.map +1 -1
  13. package/dist/locales/fr.js +5 -5
  14. package/dist/locales/fr.js.map +1 -1
  15. package/dist/locales/locale-codes.js +11 -2
  16. package/dist/locales/locale-codes.js.map +1 -1
  17. package/dist/locales/pt.js +5 -5
  18. package/dist/locales/pt.js.map +1 -1
  19. package/dist/temba-components.js +509 -87
  20. package/dist/temba-components.js.map +1 -1
  21. package/out-tsc/src/chart/TembaChart.js +136 -62
  22. package/out-tsc/src/chart/TembaChart.js.map +1 -1
  23. package/out-tsc/src/datepicker/DatePicker.js +11 -1
  24. package/out-tsc/src/datepicker/DatePicker.js.map +1 -1
  25. package/out-tsc/src/datepicker/RangePicker.js +595 -0
  26. package/out-tsc/src/datepicker/RangePicker.js.map +1 -0
  27. package/out-tsc/src/flow/Editor.js +210 -1
  28. package/out-tsc/src/flow/Editor.js.map +1 -1
  29. package/out-tsc/src/flow/EditorNode.js +98 -139
  30. package/out-tsc/src/flow/EditorNode.js.map +1 -1
  31. package/out-tsc/src/flow/StickyNote.js +272 -0
  32. package/out-tsc/src/flow/StickyNote.js.map +1 -0
  33. package/out-tsc/src/interfaces.js +1 -0
  34. package/out-tsc/src/interfaces.js.map +1 -1
  35. package/out-tsc/src/list/RunList.js +2 -1
  36. package/out-tsc/src/list/RunList.js.map +1 -1
  37. package/out-tsc/src/list/SortableList.js +9 -0
  38. package/out-tsc/src/list/SortableList.js.map +1 -1
  39. package/out-tsc/src/locales/es.js +5 -5
  40. package/out-tsc/src/locales/es.js.map +1 -1
  41. package/out-tsc/src/locales/fr.js +5 -5
  42. package/out-tsc/src/locales/fr.js.map +1 -1
  43. package/out-tsc/src/locales/locale-codes.js +11 -2
  44. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  45. package/out-tsc/src/locales/pt.js +5 -5
  46. package/out-tsc/src/locales/pt.js.map +1 -1
  47. package/out-tsc/src/store/AppState.js +33 -0
  48. package/out-tsc/src/store/AppState.js.map +1 -1
  49. package/out-tsc/src/vectoricon/index.js +2 -1
  50. package/out-tsc/src/vectoricon/index.js.map +1 -1
  51. package/out-tsc/temba-modules.js +5 -1
  52. package/out-tsc/temba-modules.js.map +1 -1
  53. package/out-tsc/test/temba-chart.test.js +36 -0
  54. package/out-tsc/test/temba-chart.test.js.map +1 -1
  55. package/out-tsc/test/temba-datepicker.test.js +1 -1
  56. package/out-tsc/test/temba-datepicker.test.js.map +1 -1
  57. package/out-tsc/test/temba-flow-editor-node.test.js +249 -5
  58. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  59. package/out-tsc/test/temba-range-picker.test.js +123 -0
  60. package/out-tsc/test/temba-range-picker.test.js.map +1 -0
  61. package/out-tsc/test/temba-select.test.js +10 -16
  62. package/out-tsc/test/temba-select.test.js.map +1 -1
  63. package/out-tsc/test/temba-webchat.test.js +4 -0
  64. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  65. package/out-tsc/test/utils.test.js +62 -0
  66. package/out-tsc/test/utils.test.js.map +1 -1
  67. package/package.json +1 -1
  68. package/screenshots/truth/datepicker/range-picker-all.png +0 -0
  69. package/screenshots/truth/datepicker/range-picker-button-states.png +0 -0
  70. package/screenshots/truth/datepicker/range-picker-default.png +0 -0
  71. package/screenshots/truth/datepicker/range-picker-editing-start.png +0 -0
  72. package/screenshots/truth/datepicker/range-picker-initial-values.png +0 -0
  73. package/screenshots/truth/datepicker/range-picker-min-max.png +0 -0
  74. package/screenshots/truth/datepicker/range-picker-week.png +0 -0
  75. package/screenshots/truth/datepicker/range-picker-year.png +0 -0
  76. package/screenshots/truth/sticky-note/blue.png +0 -0
  77. package/screenshots/truth/sticky-note/gray.png +0 -0
  78. package/screenshots/truth/sticky-note/green.png +0 -0
  79. package/screenshots/truth/sticky-note/pink.png +0 -0
  80. package/screenshots/truth/sticky-note/yellow.png +0 -0
  81. package/screenshots/truth/webchat/connected-state.png +0 -0
  82. package/src/chart/TembaChart.ts +144 -66
  83. package/src/datepicker/DatePicker.ts +9 -1
  84. package/src/datepicker/RangePicker.ts +602 -0
  85. package/src/flow/Editor.ts +252 -2
  86. package/src/flow/EditorNode.ts +98 -156
  87. package/src/flow/StickyNote.ts +284 -0
  88. package/src/interfaces.ts +2 -1
  89. package/src/list/RunList.ts +2 -1
  90. package/src/list/SortableList.ts +11 -0
  91. package/src/locales/es.ts +18 -13
  92. package/src/locales/fr.ts +18 -13
  93. package/src/locales/locale-codes.ts +11 -2
  94. package/src/locales/pt.ts +18 -13
  95. package/src/store/AppState.ts +51 -1
  96. package/src/store/flow-definition.d.ts +8 -0
  97. package/src/vectoricon/index.ts +2 -1
  98. package/static/svg/index.pdf +137 -0
  99. package/temba-modules.ts +5 -1
  100. package/test/temba-chart.test.ts +47 -0
  101. package/test/temba-datepicker.test.ts +1 -1
  102. package/test/temba-flow-editor-node.test.ts +322 -6
  103. package/test/temba-range-picker.test.ts +193 -0
  104. package/test/temba-select.test.ts +11 -19
  105. package/test/temba-webchat.test.ts +7 -0
  106. package/test/utils.test.ts +98 -0
  107. package/web-dev-server.config.mjs +30 -22
  108. package/web-test-runner.config.mjs +2 -0
  109. package/demo/datepicker/example.html +0 -69
package/temba-modules.ts CHANGED
@@ -36,7 +36,7 @@ import { TembaSlider } from './src/slider/TembaSlider';
36
36
  import { RunList } from './src/list/RunList';
37
37
  import { FlowStoreElement } from './src/store/FlowStoreElement';
38
38
  import { ContactNameFetch } from './src/contacts/ContactNameFetch';
39
- import DatePicker from './src/datepicker/DatePicker';
39
+ import { DatePicker } from './src/datepicker/DatePicker';
40
40
  import { FieldManager } from './src/fields/FieldManager';
41
41
  import { SortableList } from './src/list/SortableList';
42
42
  import { ContentMenu } from './src/list/ContentMenu';
@@ -57,6 +57,7 @@ import { Chat } from './src/chat/Chat';
57
57
  import { MediaPicker } from './src/mediapicker/MediaPicker';
58
58
  import { Editor } from './src/flow/Editor';
59
59
  import { EditorNode } from './src/flow/EditorNode';
60
+ import { StickyNote } from './src/flow/StickyNote';
60
61
  import { ContactNotepad } from './src/contacts/ContactNotepad';
61
62
  import { ProgressBar } from './src/progress/ProgressBar';
62
63
  import { StartProgress } from './src/progress/StartProgress';
@@ -65,6 +66,7 @@ import { PopupSelect } from './src/select/PopupSelect';
65
66
  import { UserSelect } from './src/select/UserSelect';
66
67
  import { WorkspaceSelect } from './src/select/WorkspaceSelect';
67
68
  import { TembaChart } from './src/chart/TembaChart';
69
+ import { RangePicker } from './src/datepicker/RangePicker';
68
70
 
69
71
  export function addCustomElement(name: string, comp: any) {
70
72
  if (!window.customElements.get(name)) {
@@ -77,6 +79,7 @@ addCustomElement('temba-alert', Alert);
77
79
  addCustomElement('temba-store', Store);
78
80
  addCustomElement('temba-textinput', TextInput);
79
81
  addCustomElement('temba-datepicker', DatePicker);
82
+ addCustomElement('temba-range-picker', RangePicker);
80
83
  addCustomElement('temba-date', TembaDate);
81
84
  addCustomElement('temba-completion', Completion);
82
85
  addCustomElement('temba-checkbox', Checkbox);
@@ -132,6 +135,7 @@ addCustomElement('temba-chat', Chat);
132
135
  addCustomElement('temba-media-picker', MediaPicker);
133
136
  addCustomElement('temba-flow-editor', Editor);
134
137
  addCustomElement('temba-flow-node', EditorNode);
138
+ addCustomElement('temba-sticky-note', StickyNote);
135
139
  addCustomElement('temba-contact-notepad', ContactNotepad);
136
140
  addCustomElement('temba-progress', ProgressBar);
137
141
  addCustomElement('temba-start-progress', StartProgress);
@@ -182,6 +182,30 @@ describe('temba-chart', () => {
182
182
  expect(chart.chart.options.scales.x.type).to.equal('category');
183
183
  expect((chart.chart.options.scales.x as any).time).to.be.undefined;
184
184
  });
185
+
186
+ it('configures scales correctly for horizontal charts', async () => {
187
+ const chart: TembaChart = await getChart();
188
+
189
+ // Test vertical chart (default)
190
+ chart.data = sampleData;
191
+ await chart.updateComplete;
192
+ await new Promise((resolve) => setTimeout(resolve, 100));
193
+
194
+ // In vertical charts: x-axis should be for categories, y-axis for values
195
+ expect((chart.chart.options.scales as any).x.type).to.equal('category');
196
+ expect((chart.chart.options.scales as any).y.min).to.equal(0);
197
+ expect((chart.chart.options.scales as any).y.stacked).to.equal(true);
198
+
199
+ // Test horizontal chart
200
+ chart.horizontal = true;
201
+ await chart.updateComplete;
202
+ await new Promise((resolve) => setTimeout(resolve, 100));
203
+
204
+ // In horizontal charts: x-axis should be for values, y-axis for categories
205
+ expect((chart.chart.options.scales as any).x.min).to.equal(0);
206
+ expect((chart.chart.options.scales as any).x.stacked).to.equal(true);
207
+ expect((chart.chart.options.scales as any).y.type).to.equal('category');
208
+ });
185
209
  });
186
210
 
187
211
  describe('formatDurationFromSeconds', () => {
@@ -232,4 +256,27 @@ describe('formatDurationFromSeconds', () => {
232
256
  expect(formatDurationFromSeconds(1209600)).to.equal('14d'); // 2 weeks
233
257
  expect(formatDurationFromSeconds(2678400)).to.equal('31d'); // ~1 month
234
258
  });
259
+
260
+ it('supports horizontal bar charts', async () => {
261
+ const chart: TembaChart = await getChart();
262
+
263
+ // Test that horizontal property defaults to false
264
+ expect(chart.horizontal).to.equal(false);
265
+
266
+ // Set horizontal to true
267
+ chart.horizontal = true;
268
+ chart.data = sampleData;
269
+ await chart.updateComplete;
270
+
271
+ // Wait for the chart to be created after data is set
272
+ await new Promise((resolve) => setTimeout(resolve, 100));
273
+
274
+ // Test that the chart was created with horizontal configuration
275
+ expect(chart.horizontal).to.equal(true);
276
+ expect(chart.chart).to.exist;
277
+
278
+ // Test that the chart configuration includes indexAxis: 'y' for horizontal bars
279
+ const chartConfig = chart.chart.options;
280
+ expect(chartConfig.indexAxis).to.equal('y');
281
+ });
235
282
  });
@@ -1,5 +1,5 @@
1
1
  import { fixture, expect, assert } from '@open-wc/testing';
2
- import DatePicker from '../src/datepicker/DatePicker';
2
+ import { DatePicker } from '../src/datepicker/DatePicker';
3
3
  import { assertScreenshot, getAttributes, getClip } from './utils.test';
4
4
 
5
5
  export const getPickerHTML = (attrs: any = {}) => {
@@ -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
  });
@@ -0,0 +1,193 @@
1
+ import { fixture, expect, assert } from '@open-wc/testing';
2
+ import { RangePicker } from '../src/datepicker/RangePicker';
3
+ import { assertScreenshot, getAttributes, getClip } from './utils.test';
4
+ import { DateTime } from 'luxon';
5
+
6
+ export const getRangePickerHTML = (attrs: any = {}) => {
7
+ return `<temba-range-picker ${getAttributes(attrs)}></temba-range-picker>`;
8
+ };
9
+
10
+ export const createRangePicker = async (def: string) => {
11
+ const parentNode = document.createElement('div');
12
+ parentNode.setAttribute('style', 'width: 600px;');
13
+ parentNode.id = 'parent';
14
+ const picker: RangePicker = await fixture(def, { parentNode });
15
+ return picker;
16
+ };
17
+
18
+ describe('temba-range-picker', () => {
19
+ it('can create a range picker', async () => {
20
+ const picker: RangePicker = await createRangePicker(getRangePickerHTML());
21
+ assert.instanceOf(picker, RangePicker);
22
+
23
+ // Should have default range (last month)
24
+ expect(picker.selectedRange).to.equal('M');
25
+ expect(picker.startDate).to.not.be.empty;
26
+ expect(picker.endDate).to.not.be.empty;
27
+
28
+ await assertScreenshot('datepicker/range-picker-default', getClip(picker));
29
+ });
30
+
31
+ it('can be initialized with start and end dates', async () => {
32
+ const picker: RangePicker = await createRangePicker(
33
+ getRangePickerHTML({ start: '2024-01-01', end: '2024-01-31' })
34
+ );
35
+
36
+ expect(picker.startDate).to.equal('2024-01-01');
37
+ expect(picker.endDate).to.equal('2024-01-31');
38
+ expect(picker.selectedRange).to.equal('');
39
+
40
+ await assertScreenshot(
41
+ 'datepicker/range-picker-initial-values',
42
+ getClip(picker)
43
+ );
44
+ });
45
+
46
+ it('can set min and max dates', async () => {
47
+ const picker: RangePicker = await createRangePicker(
48
+ getRangePickerHTML({
49
+ start: '2024-06-01',
50
+ end: '2024-06-30',
51
+ min: '2024-01-01',
52
+ max: '2024-12-31'
53
+ })
54
+ );
55
+
56
+ expect(picker.minDate).to.equal('2024-01-01');
57
+ expect(picker.maxDate).to.equal('2024-12-31');
58
+
59
+ await assertScreenshot('datepicker/range-picker-min-max', getClip(picker));
60
+ });
61
+
62
+ it('can set range using buttons', async () => {
63
+ const picker: RangePicker = await createRangePicker(getRangePickerHTML());
64
+
65
+ // Click Week button
66
+ const weekBtn = picker.shadowRoot?.querySelector(
67
+ '.range-btn'
68
+ ) as HTMLButtonElement;
69
+ weekBtn.click();
70
+ await picker.updateComplete;
71
+
72
+ expect(picker.selectedRange).to.equal('W');
73
+ expect(picker.startDate).to.equal(
74
+ DateTime.now().minus({ days: 6 }).toISODate()
75
+ );
76
+ expect(picker.endDate).to.equal(DateTime.now().toISODate());
77
+
78
+ await assertScreenshot('datepicker/range-picker-week', getClip(picker));
79
+ });
80
+
81
+ it('can set year range using button', async () => {
82
+ const picker: RangePicker = await createRangePicker(getRangePickerHTML());
83
+
84
+ // Click Year button (3rd button)
85
+ const yearBtn = picker.shadowRoot?.querySelectorAll(
86
+ '.range-btn'
87
+ )[2] as HTMLButtonElement;
88
+ yearBtn.click();
89
+ await picker.updateComplete;
90
+
91
+ expect(picker.selectedRange).to.equal('Y');
92
+ expect(picker.startDate).to.equal(
93
+ DateTime.now().minus({ years: 1 }).plus({ days: 1 }).toISODate()
94
+ );
95
+ expect(picker.endDate).to.equal(DateTime.now().toISODate());
96
+
97
+ await assertScreenshot('datepicker/range-picker-year', getClip(picker));
98
+ });
99
+
100
+ it('can set all range using button', async () => {
101
+ const picker: RangePicker = await createRangePicker(getRangePickerHTML());
102
+
103
+ // Click All button (4th button)
104
+ const allBtn = picker.shadowRoot?.querySelectorAll(
105
+ '.range-btn'
106
+ )[3] as HTMLButtonElement;
107
+ allBtn.click();
108
+ await picker.updateComplete;
109
+
110
+ expect(picker.selectedRange).to.equal('ALL');
111
+ expect(picker.startDate).to.equal('2012-01-01');
112
+ expect(picker.endDate).to.equal(DateTime.now().toISODate());
113
+
114
+ await assertScreenshot('datepicker/range-picker-all', getClip(picker));
115
+ });
116
+
117
+ it('enforces valid date ranges', async () => {
118
+ const picker: RangePicker = await createRangePicker(
119
+ getRangePickerHTML({ start: '2024-06-01', end: '2024-06-30' })
120
+ );
121
+
122
+ // Verify initial state is valid
123
+ expect(
124
+ DateTime.fromISO(picker.endDate) >= DateTime.fromISO(picker.startDate)
125
+ ).to.be.true;
126
+
127
+ // The validation logic is internal and triggered through user interaction
128
+ // We can verify the component has the correct min/max constraints
129
+ expect(picker.startDate).to.equal('2024-06-01');
130
+ expect(picker.endDate).to.equal('2024-06-30');
131
+ });
132
+
133
+ it('enforces min/max date constraints', async () => {
134
+ const picker: RangePicker = await createRangePicker(
135
+ getRangePickerHTML({ min: '2024-01-01', max: '2024-12-31' })
136
+ );
137
+
138
+ expect(picker.minDate).to.equal('2024-01-01');
139
+ expect(picker.maxDate).to.equal('2024-12-31');
140
+
141
+ // Min/max are enforced through the temba-datepicker components
142
+ // when user interacts with the date inputs
143
+ });
144
+
145
+ it('shows correct button selection states', async () => {
146
+ const picker: RangePicker = await createRangePicker(getRangePickerHTML());
147
+
148
+ // Initially should have M selected
149
+ const monthBtn = picker.shadowRoot?.querySelectorAll(
150
+ '.range-btn'
151
+ )[1] as HTMLButtonElement;
152
+ expect(monthBtn.classList.contains('selected')).to.be.true;
153
+
154
+ // Click week button
155
+ const weekBtn = picker.shadowRoot?.querySelector(
156
+ '.range-btn'
157
+ ) as HTMLButtonElement;
158
+ weekBtn.click();
159
+ await picker.updateComplete;
160
+
161
+ expect(weekBtn.classList.contains('selected')).to.be.true;
162
+ expect(monthBtn.classList.contains('selected')).to.be.false;
163
+
164
+ await assertScreenshot(
165
+ 'datepicker/range-picker-button-states',
166
+ getClip(picker)
167
+ );
168
+ });
169
+
170
+ it('can click to edit dates', async () => {
171
+ const picker: RangePicker = await createRangePicker(getRangePickerHTML());
172
+
173
+ // Click on start date display
174
+ const startDisplay = picker.shadowRoot?.querySelector(
175
+ '.date-display'
176
+ ) as HTMLElement;
177
+ startDisplay.click();
178
+ await picker.updateComplete;
179
+
180
+ expect(picker.editingStart).to.be.true;
181
+
182
+ // Should show temba-datepicker for start
183
+ const startPicker = picker.shadowRoot?.querySelector(
184
+ 'temba-datepicker.start-picker'
185
+ );
186
+ expect(startPicker).to.not.be.null;
187
+
188
+ await assertScreenshot(
189
+ 'datepicker/range-picker-editing-start',
190
+ getClip(picker)
191
+ );
192
+ });
193
+ });