@nyaruka/temba-components 0.138.4 → 0.139.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 (69) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/locales/es.js +5 -5
  3. package/dist/locales/es.js.map +1 -1
  4. package/dist/locales/fr.js +5 -5
  5. package/dist/locales/fr.js.map +1 -1
  6. package/dist/locales/locale-codes.js +2 -11
  7. package/dist/locales/locale-codes.js.map +1 -1
  8. package/dist/locales/pt.js +5 -5
  9. package/dist/locales/pt.js.map +1 -1
  10. package/dist/temba-components.js +816 -852
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/FloatingTab.js +23 -30
  13. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  14. package/out-tsc/src/flow/CanvasMenu.js +5 -3
  15. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  16. package/out-tsc/src/flow/CanvasNode.js +6 -7
  17. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  18. package/out-tsc/src/flow/Editor.js +152 -235
  19. package/out-tsc/src/flow/Editor.js.map +1 -1
  20. package/out-tsc/src/flow/Plumber.js +757 -403
  21. package/out-tsc/src/flow/Plumber.js.map +1 -1
  22. package/out-tsc/src/flow/utils.js +138 -66
  23. package/out-tsc/src/flow/utils.js.map +1 -1
  24. package/out-tsc/src/interfaces.js +1 -0
  25. package/out-tsc/src/interfaces.js.map +1 -1
  26. package/out-tsc/src/list/TicketList.js +4 -1
  27. package/out-tsc/src/list/TicketList.js.map +1 -1
  28. package/out-tsc/src/live/ContactChat.js +18 -1
  29. package/out-tsc/src/live/ContactChat.js.map +1 -1
  30. package/out-tsc/src/locales/es.js +5 -5
  31. package/out-tsc/src/locales/es.js.map +1 -1
  32. package/out-tsc/src/locales/fr.js +5 -5
  33. package/out-tsc/src/locales/fr.js.map +1 -1
  34. package/out-tsc/src/locales/locale-codes.js +2 -11
  35. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  36. package/out-tsc/src/locales/pt.js +5 -5
  37. package/out-tsc/src/locales/pt.js.map +1 -1
  38. package/out-tsc/src/simulator/Simulator.js +1 -0
  39. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  40. package/out-tsc/test/temba-floating-tab.test.js +4 -6
  41. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  42. package/out-tsc/test/temba-flow-collision.test.js +221 -223
  43. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  44. package/out-tsc/test/temba-flow-editor.test.js +0 -2
  45. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  46. package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
  47. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  48. package/out-tsc/test/temba-flow-plumber.test.js +102 -93
  49. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/display/FloatingTab.ts +22 -31
  52. package/src/flow/CanvasMenu.ts +8 -3
  53. package/src/flow/CanvasNode.ts +6 -7
  54. package/src/flow/Editor.ts +184 -279
  55. package/src/flow/Plumber.ts +1011 -457
  56. package/src/flow/utils.ts +162 -84
  57. package/src/interfaces.ts +2 -1
  58. package/src/list/TicketList.ts +4 -1
  59. package/src/live/ContactChat.ts +19 -1
  60. package/src/locales/es.ts +13 -18
  61. package/src/locales/fr.ts +13 -18
  62. package/src/locales/locale-codes.ts +2 -11
  63. package/src/locales/pt.ts +13 -18
  64. package/src/simulator/Simulator.ts +1 -0
  65. package/test/temba-floating-tab.test.ts +4 -6
  66. package/test/temba-flow-collision.test.ts +225 -303
  67. package/test/temba-flow-editor.test.ts +0 -2
  68. package/test/temba-flow-plumber-connections.test.ts +97 -97
  69. package/test/temba-flow-plumber.test.ts +116 -103
@@ -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
- const mockElement = document.createElement('div');
17
- stub(document, 'getElementById').returns(mockElement);
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
- // Restore the original document.getElementById
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 connections are found', () => {
57
- const result = plumber.setConnectionRemovingState('test-exit', true);
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('sets removing class on connections when isRemoving is true', () => {
63
- const mockConnections = [
64
- { id: 'conn1', addClass: stub() },
65
- { id: 'conn2', addClass: stub() }
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
- (plumber as any).jsPlumb.getConnections = stub().returns(mockConnections);
41
+ // Create a connection
42
+ plumber.connectIds('node-1', 'exit-1', 'target-1');
43
+ clock.tick(16);
69
44
 
70
- const result = plumber.setConnectionRemovingState('test-exit', true);
45
+ const result = plumber.setConnectionRemovingState('exit-1', true);
71
46
  expect(result).to.be.true;
72
- expect(mockConnections[0].addClass).to.have.been.calledWith('removing');
73
- expect(mockConnections[1].addClass).to.have.been.calledWith('removing');
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 from connections when isRemoving is false', () => {
77
- const mockConnections = [
78
- { id: 'conn1', removeClass: stub() },
79
- { id: 'conn2', removeClass: stub() }
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
- (plumber as any).jsPlumb.getConnections = stub().returns(mockConnections);
63
+ plumber.connectIds('node-1', 'exit-2', 'target-2');
64
+ clock.tick(16);
83
65
 
84
- const result = plumber.setConnectionRemovingState('test-exit', false);
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(mockConnections[0].removeClass).to.have.been.calledWith(
87
- 'removing'
88
- );
89
- expect(mockConnections[1].removeClass).to.have.been.calledWith(
90
- 'removing'
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 timeout when called multiple times', () => {
113
- // Set up spies for window.setTimeout and window.clearTimeout instead of global
114
- const clearTimeoutSpy = stub(window, 'clearTimeout');
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
- // Should have called clearTimeout once and setTimeout twice
122
- expect(clearTimeoutSpy).to.have.been.calledOnce;
123
- expect(setTimeoutSpy).to.have.been.calledTwice;
118
+ expect(cancelSpy).to.have.been.calledOnce;
119
+ expect(rafSpy).to.have.been.calledTwice;
124
120
 
125
- // Clean up
126
- clearTimeoutSpy.restore();
127
- setTimeoutSpy.restore();
121
+ cancelSpy.restore();
122
+ rafSpy.restore();
128
123
  });
129
124
  });
130
125
 
131
- describe('removeExitConnection', () => {
132
- it('removes connections for an exit', () => {
133
- const mockConnections = [{ id: 'conn1' }, { id: 'conn2' }];
134
- (plumber as any).jsPlumb.getConnections = stub().returns(mockConnections);
135
-
136
- const result = plumber.removeExitConnection('test-exit');
137
-
138
- expect(result).to.be.true;
139
- expect((plumber as any).jsPlumb.deleteConnection).to.have.been
140
- .calledTwice;
141
- });
142
-
143
- it('returns false when no connections exist', () => {
144
- (plumber as any).jsPlumb.getConnections = stub().returns([]);
145
-
146
- const result = plumber.removeExitConnection('test-exit');
147
-
148
- expect(result).to.be.false;
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, SOURCE_DEFAULTS, TARGET_DEFAULTS } from '../src/flow/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
- const mockElement = document.createElement('div');
18
- stub(document, 'getElementById').returns(mockElement);
15
+ mockCanvas.id = 'canvas';
16
+ document.body.appendChild(mockCanvas);
19
17
 
20
- // Create a mock editor with fireCustomEvent
21
- const mockEditor = { fireCustomEvent: stub() };
18
+ mockElement = document.createElement('div');
19
+ mockElement.id = 'test-exit';
20
+ mockCanvas.appendChild(mockElement);
22
21
 
23
- // Create a new plumber instance
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
- // Restore the original document.getElementById
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('makeTarget', () => {
68
- it('creates a target endpoint for the specified element', () => {
69
- plumber.makeTarget('test-target');
70
- expect(mockJsPlumb.addEndpoint).to.have.been.called;
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('makeSource', () => {
75
- it('creates a source endpoint for the specified element', () => {
76
- plumber.makeSource('test-source');
77
- expect(mockJsPlumb.addEndpoint).to.have.been.called;
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('processes pending connections with timeout', () => {
98
- // Add a connection to pending connections
99
- plumber.connectIds('test-node', 'test-from', 'test-to');
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
- expect(mockJsPlumb.batch).to.have.been.called;
105
- });
84
+ plumber.processPendingConnections();
85
+ plumber.processPendingConnections();
106
86
 
107
- it('creates endpoints and connections for pending connections', () => {
108
- plumber.connectIds('test-node', 'test-from', 'test-to');
87
+ expect(cancelSpy).to.have.been.calledOnce;
88
+ expect(rafSpy).to.have.been.calledTwice;
109
89
 
110
- // Fast-forward clock past the timeout
111
- clock.tick(51); // Just past the 50ms timeout
90
+ cancelSpy.restore();
91
+ rafSpy.restore();
92
+ });
93
+ });
112
94
 
113
- expect(mockJsPlumb.addEndpoint).to.have.been.called;
114
- expect(mockJsPlumb.connect).to.have.been.called;
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
- it('clears existing timeout when called multiple times', () => {
118
- // Set up spies for window.setTimeout and window.clearTimeout
119
- const clearTimeoutSpy = stub(window, 'clearTimeout');
120
- const setTimeoutSpy = stub(window, 'setTimeout').returns(123 as any);
113
+ describe('reset', () => {
114
+ it('clears all state', () => {
115
+ plumber.connectIds('test-node', 'test-from', 'test-to');
116
+ plumber.reset();
121
117
 
122
- // Call twice
123
- plumber.processPendingConnections();
124
- plumber.processPendingConnections();
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
- // Should have called clearTimeout once and setTimeout twice
127
- expect(clearTimeoutSpy).to.have.been.calledOnce;
128
- expect(setTimeoutSpy).to.have.been.calledTwice;
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
- // Clean up
131
- clearTimeoutSpy.restore();
132
- setTimeoutSpy.restore();
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
- it('handles empty pending connections', () => {
136
- // Call without adding any connections
137
- plumber.processPendingConnections();
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
- // Fast-forward clock past the timeout
140
- clock.tick(51); // Just past the 50ms timeout
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
- expect(mockJsPlumb.batch).to.have.been.called;
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
- describe('constants', () => {
147
- it('has correct properties in SOURCE_DEFAULTS', () => {
148
- expect(SOURCE_DEFAULTS).to.have.property('endpoint');
149
- expect(SOURCE_DEFAULTS).to.have.property('anchors');
150
- expect(SOURCE_DEFAULTS).to.have.property('maxConnections');
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
- it('has correct properties in TARGET_DEFAULTS', () => {
155
- expect(TARGET_DEFAULTS).to.have.property('endpoint');
156
- expect(TARGET_DEFAULTS).to.have.property('anchor');
157
- expect(TARGET_DEFAULTS).to.have.property('maxConnections');
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
  });