@nyaruka/temba-components 0.138.6 → 0.140.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/.github/workflows/cla.yml +1 -1
- package/.github/workflows/copilot-setup-steps.yml +6 -1
- package/CHANGELOG.md +26 -0
- package/demo/data/flows/sample-flow.json +24 -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 +2 -11
- 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 +1112 -882
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +10 -7
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Dropdown.js +3 -1
- package/out-tsc/src/display/Dropdown.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +25 -32
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +163 -5
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +5 -3
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +70 -29
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +290 -239
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +118 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +757 -403
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +13 -4
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/audio-player.js +112 -0
- package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
- package/out-tsc/src/flow/actions/enter_flow.js +43 -0
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
- package/out-tsc/src/flow/actions/play_audio.js +57 -4
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +86 -3
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/config.js +11 -3
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
- package/out-tsc/src/flow/nodes/terminal.js +7 -0
- package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/operators.js +21 -5
- package/out-tsc/src/flow/operators.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +213 -65
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +4 -2
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +49 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +2 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +52 -7
- package/out-tsc/src/layout/Dialog.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +4 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/live/TembaChart.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 +2 -11
- 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/simulator/Simulator.js +10 -3
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +89 -3
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/actions/play_audio.test.js +118 -0
- package/out-tsc/test/actions/play_audio.test.js.map +1 -0
- package/out-tsc/test/actions/say_msg.test.js +158 -0
- package/out-tsc/test/actions/say_msg.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
- package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
- package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
- package/out-tsc/test/temba-floating-tab.test.js +4 -6
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +473 -220
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +0 -2
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +102 -93
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +6 -6
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
- package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/src/display/Chat.ts +13 -7
- package/src/display/Dropdown.ts +3 -1
- package/src/display/FloatingTab.ts +24 -33
- package/src/display/Thumbnail.ts +162 -2
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +75 -30
- package/src/flow/Editor.ts +336 -288
- package/src/flow/NodeEditor.ts +137 -9
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/StickyNote.ts +14 -4
- package/src/flow/actions/audio-player.ts +127 -0
- package/src/flow/actions/enter_flow.ts +44 -0
- package/src/flow/actions/play_audio.ts +64 -5
- package/src/flow/actions/say_msg.ts +94 -4
- package/src/flow/config.ts +11 -3
- package/src/flow/nodes/shared-rules.ts +1 -1
- package/src/flow/nodes/terminal.ts +9 -0
- package/src/flow/nodes/wait_for_audio.ts +88 -0
- package/src/flow/nodes/wait_for_dial.ts +176 -0
- package/src/flow/nodes/wait_for_digits.ts +86 -2
- package/src/flow/nodes/wait_for_menu.ts +209 -3
- package/src/flow/operators.ts +23 -5
- package/src/flow/types.ts +23 -1
- package/src/flow/utils.ts +238 -81
- package/src/form/ArrayEditor.ts +4 -2
- package/src/form/FieldRenderer.ts +64 -1
- package/src/interfaces.ts +3 -1
- package/src/layout/Dialog.ts +53 -7
- package/src/list/TicketList.ts +4 -1
- package/src/live/TembaChart.ts +1 -1
- package/src/locales/es.ts +13 -18
- package/src/locales/fr.ts +13 -18
- package/src/locales/locale-codes.ts +2 -11
- package/src/locales/pt.ts +13 -18
- package/src/simulator/Simulator.ts +13 -3
- package/src/store/AppState.ts +105 -1
- package/src/store/flow-definition.d.ts +2 -0
- package/test/actions/play_audio.test.ts +155 -0
- package/test/actions/say_msg.test.ts +196 -0
- package/test/nodes/wait_for_audio.test.ts +182 -0
- package/test/nodes/wait_for_dial.test.ts +382 -0
- package/test/nodes/wait_for_digits.test.ts +233 -109
- package/test/nodes/wait_for_menu.test.ts +383 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +495 -293
- package/test/temba-flow-editor.test.ts +0 -2
- package/test/temba-flow-plumber-connections.test.ts +97 -97
- package/test/temba-flow-plumber.test.ts +116 -103
- package/test/temba-node-type-selector.test.ts +6 -6
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
|
@@ -30,8 +30,6 @@ describe('Editor', () => {
|
|
|
30
30
|
expect(styles.cssText).to.contain('#editor');
|
|
31
31
|
expect(styles.cssText).to.contain('#grid');
|
|
32
32
|
expect(styles.cssText).to.contain('#canvas');
|
|
33
|
-
expect(styles.cssText).to.contain('.plumb-source');
|
|
34
|
-
expect(styles.cssText).to.contain('.plumb-target');
|
|
35
33
|
expect(styles.cssText).to.contain('.plumb-connector');
|
|
36
34
|
});
|
|
37
35
|
|
|
@@ -8,144 +8,144 @@ describe('Plumber - Connection Management', () => {
|
|
|
8
8
|
let clock: SinonFakeTimers;
|
|
9
9
|
|
|
10
10
|
beforeEach(() => {
|
|
11
|
-
// Use fake timers to control setTimeout
|
|
12
11
|
clock = useFakeTimers();
|
|
13
12
|
|
|
14
|
-
// Create mock canvas and make getElementById return a mock element
|
|
15
13
|
mockCanvas = document.createElement('div');
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
mockCanvas.id = 'canvas';
|
|
15
|
+
document.body.appendChild(mockCanvas);
|
|
18
16
|
|
|
19
|
-
// Create a mock editor with fireCustomEvent
|
|
20
17
|
const mockEditor = { fireCustomEvent: stub() };
|
|
21
|
-
|
|
22
|
-
// Create a new plumber instance
|
|
23
18
|
plumber = new Plumber(mockCanvas, mockEditor);
|
|
24
|
-
|
|
25
|
-
// Replace the internal jsPlumb instance with mocks
|
|
26
|
-
(plumber as any).jsPlumb = {
|
|
27
|
-
getConnections: stub().returns([]),
|
|
28
|
-
addClass: stub(),
|
|
29
|
-
removeClass: stub(),
|
|
30
|
-
batch: stub().callsFake((fn: any) => fn()),
|
|
31
|
-
addEndpoint: stub().returns({}),
|
|
32
|
-
revalidate: stub(),
|
|
33
|
-
connect: stub(),
|
|
34
|
-
getEndpoints: stub().returns([
|
|
35
|
-
{ elementId: 'test-from', addClass: stub() }
|
|
36
|
-
]),
|
|
37
|
-
select: stub().returns({
|
|
38
|
-
deleteAll: stub()
|
|
39
|
-
}),
|
|
40
|
-
selectEndpoints: stub().returns({
|
|
41
|
-
deleteAll: stub()
|
|
42
|
-
}),
|
|
43
|
-
deleteConnection: stub(),
|
|
44
|
-
removeAllEndpoints: stub(),
|
|
45
|
-
repaintEverything: stub()
|
|
46
|
-
};
|
|
47
19
|
});
|
|
48
20
|
|
|
49
21
|
afterEach(() => {
|
|
50
|
-
|
|
51
|
-
(document.getElementById as any).restore?.();
|
|
22
|
+
mockCanvas.remove();
|
|
52
23
|
clock.restore();
|
|
53
24
|
});
|
|
54
25
|
|
|
55
26
|
describe('setConnectionRemovingState', () => {
|
|
56
|
-
it('returns false when no
|
|
57
|
-
const result = plumber.setConnectionRemovingState('
|
|
27
|
+
it('returns false when no connection exists for the exit', () => {
|
|
28
|
+
const result = plumber.setConnectionRemovingState('nonexistent', true);
|
|
58
29
|
expect(result).to.be.false;
|
|
59
|
-
expect((plumber as any).jsPlumb.getConnections).to.have.been.called;
|
|
60
30
|
});
|
|
61
31
|
|
|
62
|
-
it('
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
32
|
+
it('adds removing class when isRemoving is true', () => {
|
|
33
|
+
// Create mock elements for a connection
|
|
34
|
+
const exitEl = document.createElement('div');
|
|
35
|
+
exitEl.id = 'exit-1';
|
|
36
|
+
const targetEl = document.createElement('div');
|
|
37
|
+
targetEl.id = 'target-1';
|
|
38
|
+
mockCanvas.appendChild(exitEl);
|
|
39
|
+
mockCanvas.appendChild(targetEl);
|
|
67
40
|
|
|
68
|
-
|
|
41
|
+
// Create a connection
|
|
42
|
+
plumber.connectIds('node-1', 'exit-1', 'target-1');
|
|
43
|
+
clock.tick(16);
|
|
69
44
|
|
|
70
|
-
const result = plumber.setConnectionRemovingState('
|
|
45
|
+
const result = plumber.setConnectionRemovingState('exit-1', true);
|
|
71
46
|
expect(result).to.be.true;
|
|
72
|
-
|
|
73
|
-
|
|
47
|
+
|
|
48
|
+
const conn = (plumber as any).connections.get('exit-1');
|
|
49
|
+
expect(conn.svgEl.classList.contains('removing')).to.be.true;
|
|
50
|
+
|
|
51
|
+
exitEl.remove();
|
|
52
|
+
targetEl.remove();
|
|
74
53
|
});
|
|
75
54
|
|
|
76
|
-
it('removes removing class
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
55
|
+
it('removes removing class when isRemoving is false', () => {
|
|
56
|
+
const exitEl = document.createElement('div');
|
|
57
|
+
exitEl.id = 'exit-2';
|
|
58
|
+
const targetEl = document.createElement('div');
|
|
59
|
+
targetEl.id = 'target-2';
|
|
60
|
+
mockCanvas.appendChild(exitEl);
|
|
61
|
+
mockCanvas.appendChild(targetEl);
|
|
81
62
|
|
|
82
|
-
|
|
63
|
+
plumber.connectIds('node-1', 'exit-2', 'target-2');
|
|
64
|
+
clock.tick(16);
|
|
83
65
|
|
|
84
|
-
|
|
66
|
+
plumber.setConnectionRemovingState('exit-2', true);
|
|
67
|
+
plumber.setConnectionRemovingState('exit-2', false);
|
|
68
|
+
|
|
69
|
+
const conn = (plumber as any).connections.get('exit-2');
|
|
70
|
+
expect(conn.svgEl.classList.contains('removing')).to.be.false;
|
|
71
|
+
|
|
72
|
+
exitEl.remove();
|
|
73
|
+
targetEl.remove();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('removeExitConnection', () => {
|
|
78
|
+
it('removes a connection for an exit', () => {
|
|
79
|
+
const exitEl = document.createElement('div');
|
|
80
|
+
exitEl.id = 'exit-3';
|
|
81
|
+
const targetEl = document.createElement('div');
|
|
82
|
+
targetEl.id = 'target-3';
|
|
83
|
+
mockCanvas.appendChild(exitEl);
|
|
84
|
+
mockCanvas.appendChild(targetEl);
|
|
85
|
+
|
|
86
|
+
plumber.connectIds('node-1', 'exit-3', 'target-3');
|
|
87
|
+
clock.tick(16);
|
|
88
|
+
|
|
89
|
+
expect((plumber as any).connections.has('exit-3')).to.be.true;
|
|
90
|
+
|
|
91
|
+
const result = plumber.removeExitConnection('exit-3');
|
|
85
92
|
expect(result).to.be.true;
|
|
86
|
-
expect(
|
|
87
|
-
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
expect((plumber as any).connections.has('exit-3')).to.be.false;
|
|
94
|
+
|
|
95
|
+
exitEl.remove();
|
|
96
|
+
targetEl.remove();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns false when no connection exists', () => {
|
|
100
|
+
const result = plumber.removeExitConnection('nonexistent');
|
|
101
|
+
expect(result).to.be.false;
|
|
92
102
|
});
|
|
93
103
|
});
|
|
94
104
|
|
|
95
105
|
describe('connectIds and processPendingConnections', () => {
|
|
96
106
|
it('adds connection to pending connections', () => {
|
|
97
|
-
// Call connectIds which should add to pending connections
|
|
98
107
|
plumber.connectIds('test-node', 'test-from', 'test-to');
|
|
99
|
-
|
|
100
|
-
// Verify pendingConnections has the new connection
|
|
101
108
|
expect((plumber as any).pendingConnections.length).to.equal(1);
|
|
102
|
-
|
|
103
|
-
// Advance timer to trigger the timeout
|
|
104
|
-
clock.tick(51); // Just past the 50ms timeout
|
|
105
|
-
|
|
106
|
-
// Now the batch should have been called
|
|
107
|
-
expect((plumber as any).jsPlumb.batch).to.have.been.called;
|
|
108
|
-
expect((plumber as any).jsPlumb.addEndpoint).to.have.been.called;
|
|
109
|
-
expect((plumber as any).jsPlumb.connect).to.have.been.called;
|
|
110
109
|
});
|
|
111
110
|
|
|
112
|
-
it('clears previous
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const setTimeoutSpy = stub(window, 'setTimeout').returns(123 as any);
|
|
111
|
+
it('clears previous rAF when called multiple times', () => {
|
|
112
|
+
const cancelSpy = stub(window, 'cancelAnimationFrame');
|
|
113
|
+
const rafSpy = stub(window, 'requestAnimationFrame').returns(123 as any);
|
|
116
114
|
|
|
117
|
-
// Call twice
|
|
118
115
|
plumber.processPendingConnections();
|
|
119
116
|
plumber.processPendingConnections();
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
expect(
|
|
123
|
-
expect(setTimeoutSpy).to.have.been.calledTwice;
|
|
118
|
+
expect(cancelSpy).to.have.been.calledOnce;
|
|
119
|
+
expect(rafSpy).to.have.been.calledTwice;
|
|
124
120
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
setTimeoutSpy.restore();
|
|
121
|
+
cancelSpy.restore();
|
|
122
|
+
rafSpy.restore();
|
|
128
123
|
});
|
|
129
124
|
});
|
|
130
125
|
|
|
131
|
-
describe('
|
|
132
|
-
it('removes connections for
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
126
|
+
describe('removeNodeConnections', () => {
|
|
127
|
+
it('removes inbound and outbound connections for a node', () => {
|
|
128
|
+
const exitEl = document.createElement('div');
|
|
129
|
+
exitEl.id = 'exit-4';
|
|
130
|
+
exitEl.classList.add('exit');
|
|
131
|
+
const nodeEl = document.createElement('div');
|
|
132
|
+
nodeEl.id = 'node-1';
|
|
133
|
+
nodeEl.appendChild(exitEl);
|
|
134
|
+
const targetEl = document.createElement('div');
|
|
135
|
+
targetEl.id = 'target-4';
|
|
136
|
+
mockCanvas.appendChild(nodeEl);
|
|
137
|
+
mockCanvas.appendChild(targetEl);
|
|
138
|
+
|
|
139
|
+
plumber.connectIds('node-1', 'exit-4', 'target-4');
|
|
140
|
+
clock.tick(16);
|
|
141
|
+
|
|
142
|
+
expect((plumber as any).connections.size).to.equal(1);
|
|
143
|
+
|
|
144
|
+
plumber.removeNodeConnections('node-1', ['exit-4']);
|
|
145
|
+
expect((plumber as any).connections.size).to.equal(0);
|
|
146
|
+
|
|
147
|
+
nodeEl.remove();
|
|
148
|
+
targetEl.remove();
|
|
149
149
|
});
|
|
150
150
|
});
|
|
151
151
|
});
|
|
@@ -1,60 +1,30 @@
|
|
|
1
1
|
import { expect } from '@open-wc/testing';
|
|
2
|
-
import { Plumber,
|
|
2
|
+
import { Plumber, calculateFlowchartPath } from '../src/flow/Plumber';
|
|
3
3
|
import { stub, useFakeTimers, SinonFakeTimers } from 'sinon';
|
|
4
4
|
|
|
5
5
|
describe('Plumber', () => {
|
|
6
6
|
let plumber: Plumber;
|
|
7
|
-
let mockJsPlumb: any;
|
|
8
7
|
let mockCanvas: HTMLElement;
|
|
9
8
|
let clock: SinonFakeTimers;
|
|
9
|
+
let mockElement: HTMLElement;
|
|
10
10
|
|
|
11
11
|
beforeEach(() => {
|
|
12
|
-
// Use fake timers to control setTimeout
|
|
13
12
|
clock = useFakeTimers();
|
|
14
13
|
|
|
15
|
-
// Create mock canvas and make getElementById return a mock element
|
|
16
14
|
mockCanvas = document.createElement('div');
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
mockCanvas.id = 'canvas';
|
|
16
|
+
document.body.appendChild(mockCanvas);
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
mockElement = document.createElement('div');
|
|
19
|
+
mockElement.id = 'test-exit';
|
|
20
|
+
mockCanvas.appendChild(mockElement);
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
const mockEditor = { fireCustomEvent: stub() };
|
|
24
23
|
plumber = new Plumber(mockCanvas, mockEditor);
|
|
25
|
-
|
|
26
|
-
// Replace the internal jsPlumb instance with mocks
|
|
27
|
-
mockJsPlumb = {
|
|
28
|
-
getConnections: stub().returns([]),
|
|
29
|
-
addClass: stub(),
|
|
30
|
-
removeClass: stub(),
|
|
31
|
-
batch: stub().callsFake((fn) => fn()),
|
|
32
|
-
addEndpoint: stub().returns({}),
|
|
33
|
-
connect: stub(),
|
|
34
|
-
getEndpoints: stub().returns([
|
|
35
|
-
{ elementId: 'test-from', addClass: stub() }
|
|
36
|
-
]),
|
|
37
|
-
select: stub().returns({
|
|
38
|
-
deleteAll: stub()
|
|
39
|
-
}),
|
|
40
|
-
selectEndpoints: stub().returns({
|
|
41
|
-
deleteAll: stub()
|
|
42
|
-
}),
|
|
43
|
-
deleteConnection: stub(),
|
|
44
|
-
removeAllEndpoints: stub(),
|
|
45
|
-
repaintEverything: stub(),
|
|
46
|
-
revalidate: stub(),
|
|
47
|
-
bind: stub()
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
(plumber as any).jsPlumb = mockJsPlumb;
|
|
51
|
-
// Reset the connectionWait to avoid timing issues
|
|
52
|
-
(plumber as any).connectionWait = null;
|
|
53
24
|
});
|
|
54
25
|
|
|
55
26
|
afterEach(() => {
|
|
56
|
-
|
|
57
|
-
(document.getElementById as any).restore?.();
|
|
27
|
+
mockCanvas.remove();
|
|
58
28
|
clock.restore();
|
|
59
29
|
});
|
|
60
30
|
|
|
@@ -64,98 +34,141 @@ describe('Plumber', () => {
|
|
|
64
34
|
});
|
|
65
35
|
});
|
|
66
36
|
|
|
67
|
-
describe('
|
|
68
|
-
it('
|
|
69
|
-
|
|
70
|
-
|
|
37
|
+
describe('makeSource', () => {
|
|
38
|
+
it('registers a mousedown listener on the exit element', () => {
|
|
39
|
+
const exitEl = document.createElement('div');
|
|
40
|
+
exitEl.id = 'exit-1';
|
|
41
|
+
mockCanvas.appendChild(exitEl);
|
|
42
|
+
|
|
43
|
+
plumber.makeSource('exit-1');
|
|
44
|
+
|
|
45
|
+
// Source should be tracked
|
|
46
|
+
expect((plumber as any).sources.has('exit-1')).to.be.true;
|
|
47
|
+
|
|
48
|
+
exitEl.remove();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('cleans up previous listener when called again', () => {
|
|
52
|
+
const exitEl = document.createElement('div');
|
|
53
|
+
exitEl.id = 'exit-2';
|
|
54
|
+
mockCanvas.appendChild(exitEl);
|
|
55
|
+
|
|
56
|
+
plumber.makeSource('exit-2');
|
|
57
|
+
plumber.makeSource('exit-2');
|
|
58
|
+
|
|
59
|
+
expect((plumber as any).sources.has('exit-2')).to.be.true;
|
|
60
|
+
|
|
61
|
+
exitEl.remove();
|
|
71
62
|
});
|
|
72
63
|
});
|
|
73
64
|
|
|
74
|
-
describe('
|
|
75
|
-
it('
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
describe('makeTarget', () => {
|
|
66
|
+
it('is a no-op', () => {
|
|
67
|
+
// Should not throw
|
|
68
|
+
plumber.makeTarget('test-node');
|
|
78
69
|
});
|
|
79
70
|
});
|
|
80
71
|
|
|
81
72
|
describe('connectIds', () => {
|
|
82
73
|
it('adds connection to pending connections and processes them', () => {
|
|
83
74
|
plumber.connectIds('test-node', 'test-from', 'test-to');
|
|
84
|
-
|
|
85
|
-
// Verify pendingConnections has the new connection
|
|
86
75
|
expect((plumber as any).pendingConnections.length).to.equal(1);
|
|
87
|
-
|
|
88
|
-
// Advance timer to trigger the timeout
|
|
89
|
-
clock.tick(51); // Just past the 50ms timeout
|
|
90
|
-
|
|
91
|
-
// Now the batch should have been called
|
|
92
|
-
expect(mockJsPlumb.batch).to.have.been.called;
|
|
93
76
|
});
|
|
94
77
|
});
|
|
95
78
|
|
|
96
79
|
describe('processPendingConnections', () => {
|
|
97
|
-
it('
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Fast-forward clock past the timeout
|
|
102
|
-
clock.tick(51); // Just past the 50ms timeout
|
|
80
|
+
it('clears existing rAF when called multiple times', () => {
|
|
81
|
+
const cancelSpy = stub(window, 'cancelAnimationFrame');
|
|
82
|
+
const rafSpy = stub(window, 'requestAnimationFrame').returns(123 as any);
|
|
103
83
|
|
|
104
|
-
|
|
105
|
-
|
|
84
|
+
plumber.processPendingConnections();
|
|
85
|
+
plumber.processPendingConnections();
|
|
106
86
|
|
|
107
|
-
|
|
108
|
-
|
|
87
|
+
expect(cancelSpy).to.have.been.calledOnce;
|
|
88
|
+
expect(rafSpy).to.have.been.calledTwice;
|
|
109
89
|
|
|
110
|
-
|
|
111
|
-
|
|
90
|
+
cancelSpy.restore();
|
|
91
|
+
rafSpy.restore();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
112
94
|
|
|
113
|
-
|
|
114
|
-
|
|
95
|
+
describe('event system', () => {
|
|
96
|
+
it('supports on/off/notify pattern', () => {
|
|
97
|
+
let received = null;
|
|
98
|
+
const handler = (info: any) => {
|
|
99
|
+
received = info;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
plumber.on('test-event', handler);
|
|
103
|
+
(plumber as any).notifyListeners('test-event', { data: 'test' });
|
|
104
|
+
expect(received).to.deep.equal({ data: 'test' });
|
|
105
|
+
|
|
106
|
+
received = null;
|
|
107
|
+
plumber.off('test-event', handler);
|
|
108
|
+
(plumber as any).notifyListeners('test-event', { data: 'test2' });
|
|
109
|
+
expect(received).to.be.null;
|
|
115
110
|
});
|
|
111
|
+
});
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
describe('reset', () => {
|
|
114
|
+
it('clears all state', () => {
|
|
115
|
+
plumber.connectIds('test-node', 'test-from', 'test-to');
|
|
116
|
+
plumber.reset();
|
|
121
117
|
|
|
122
|
-
|
|
123
|
-
plumber.
|
|
124
|
-
plumber.
|
|
118
|
+
expect((plumber as any).pendingConnections.length).to.equal(0);
|
|
119
|
+
expect((plumber as any).connections.size).to.equal(0);
|
|
120
|
+
expect((plumber as any).sources.size).to.equal(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
describe('calculateFlowchartPath', () => {
|
|
126
|
+
it('generates a straight vertical path when source and target are aligned', () => {
|
|
127
|
+
const path = calculateFlowchartPath(100, 0, 100, 100);
|
|
128
|
+
expect(path).to.include('M 100 0');
|
|
129
|
+
expect(path).to.include('L 100 100');
|
|
130
|
+
// Should not contain Q (quadratic curve) for aligned points
|
|
131
|
+
expect(path).to.not.include('Q');
|
|
132
|
+
});
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
it('generates a path with corners when source and target are offset', () => {
|
|
135
|
+
const path = calculateFlowchartPath(50, 0, 150, 200);
|
|
136
|
+
expect(path).to.include('M 50 0');
|
|
137
|
+
expect(path).to.include('Q'); // Should have rounded corners
|
|
138
|
+
expect(path).to.include('L 150 200');
|
|
139
|
+
});
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
it('handles custom stub and corner radius', () => {
|
|
142
|
+
const path = calculateFlowchartPath(0, 0, 100, 100, 30, 15, 10);
|
|
143
|
+
expect(path).to.include('M 0 0');
|
|
144
|
+
expect(path).to.include('L 100 100');
|
|
145
|
+
});
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
it('handles cases where vertical space is tight by using reduced-radius corners', () => {
|
|
148
|
+
// With stubs of 20+10=30, and only 35 total vertical space, there's only 5px for corners
|
|
149
|
+
const path = calculateFlowchartPath(50, 0, 150, 35);
|
|
150
|
+
expect(path).to.include('M 50 0');
|
|
151
|
+
// Should still use rounded corners (L-shape with curves)
|
|
152
|
+
expect(path).to.include('Q');
|
|
153
|
+
});
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
it('enforces midY is always below source exit for top face', () => {
|
|
156
|
+
// Target above source — midY should not go above sourceY + stubStart
|
|
157
|
+
const path = calculateFlowchartPath(50, 100, 150, 50);
|
|
158
|
+
expect(path).to.include('M 50 100');
|
|
159
|
+
// Should still exit downward with a curve at exitY (120)
|
|
160
|
+
expect(path).to.include('Q');
|
|
144
161
|
});
|
|
145
162
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
expect(SOURCE_DEFAULTS).to.have.property('source');
|
|
152
|
-
});
|
|
163
|
+
it('generates a path entering from the left face', () => {
|
|
164
|
+
const path = calculateFlowchartPath(50, 0, 150, 100, 20, 10, 5, 'left');
|
|
165
|
+
expect(path).to.include('M 50 0');
|
|
166
|
+
expect(path).to.include('L 150 100'); // ends at target
|
|
167
|
+
});
|
|
153
168
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
expect(TARGET_DEFAULTS).to.have.property('target');
|
|
159
|
-
});
|
|
169
|
+
it('generates a path entering from the right face', () => {
|
|
170
|
+
const path = calculateFlowchartPath(150, 0, 50, 100, 20, 10, 5, 'right');
|
|
171
|
+
expect(path).to.include('M 150 0');
|
|
172
|
+
expect(path).to.include('L 50 100'); // ends at target
|
|
160
173
|
});
|
|
161
174
|
});
|
|
@@ -163,9 +163,9 @@ describe('temba-node-type-selector', () => {
|
|
|
163
163
|
item.textContent?.trim()
|
|
164
164
|
);
|
|
165
165
|
|
|
166
|
-
// voice flow should have Say Message and Play
|
|
166
|
+
// voice flow should have Say Message and Play Recording
|
|
167
167
|
expect(titles).to.include('Say Message');
|
|
168
|
-
expect(titles).to.include('Play
|
|
168
|
+
expect(titles).to.include('Play Recording');
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
it('filters actions by flow type - message flow should not show voice-only actions', async () => {
|
|
@@ -181,9 +181,9 @@ describe('temba-node-type-selector', () => {
|
|
|
181
181
|
item.textContent?.trim()
|
|
182
182
|
);
|
|
183
183
|
|
|
184
|
-
// message flow should not have Say Message or Play
|
|
184
|
+
// message flow should not have Say Message or Play Recording
|
|
185
185
|
expect(titles).to.not.include('Say Message');
|
|
186
|
-
expect(titles).to.not.include('Play
|
|
186
|
+
expect(titles).to.not.include('Play Recording');
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
it('filters splits by flow type - message flow should show wait for response', async () => {
|
|
@@ -219,9 +219,9 @@ describe('temba-node-type-selector', () => {
|
|
|
219
219
|
// voice flow should not have Wait for Response
|
|
220
220
|
expect(titles).to.not.include('Wait for Response');
|
|
221
221
|
|
|
222
|
-
// but should have Wait for Digits and Wait for Menu
|
|
222
|
+
// but should have Wait for Digits and Wait for Menu
|
|
223
223
|
expect(titles).to.include('Wait for Digits');
|
|
224
|
-
expect(titles).to.include('Wait for Menu
|
|
224
|
+
expect(titles).to.include('Wait for Menu');
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
it('filters by features - AI feature enables AI splits', async () => {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|