@nyaruka/temba-components 0.137.0 → 0.138.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 (37) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/temba-components.js +392 -258
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/display/FloatingTab.js +2 -2
  5. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  6. package/out-tsc/src/flow/CanvasNode.js +45 -24
  7. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  8. package/out-tsc/src/flow/Editor.js +305 -16
  9. package/out-tsc/src/flow/Editor.js.map +1 -1
  10. package/out-tsc/src/flow/Plumber.js +110 -64
  11. package/out-tsc/src/flow/Plumber.js.map +1 -1
  12. package/out-tsc/src/simulator/Simulator.js +11 -4
  13. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  14. package/out-tsc/src/store/AppState.js +12 -2
  15. package/out-tsc/src/store/AppState.js.map +1 -1
  16. package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
  17. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  18. package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
  19. package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
  20. package/out-tsc/test/temba-flow-editor.test.js +14 -10
  21. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  22. package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
  23. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  24. package/out-tsc/test/temba-flow-plumber.test.js +6 -0
  25. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/display/FloatingTab.ts +2 -2
  28. package/src/flow/CanvasNode.ts +54 -29
  29. package/src/flow/Editor.ts +357 -17
  30. package/src/flow/Plumber.ts +123 -69
  31. package/src/simulator/Simulator.ts +11 -5
  32. package/src/store/AppState.ts +13 -2
  33. package/test/temba-flow-editor-node.test.ts +2 -1
  34. package/test/temba-flow-editor-revisions.test.ts +134 -0
  35. package/test/temba-flow-editor.test.ts +16 -10
  36. package/test/temba-flow-plumber-connections.test.ts +7 -1
  37. package/test/temba-flow-plumber.test.ts +6 -0
@@ -89,50 +89,54 @@ export class Plumber {
89
89
  private showContactsTimeout: number | null = null;
90
90
  private editor: any;
91
91
 
92
- constructor(canvas: HTMLElement, editor: any) {
93
- this.editor = editor;
94
- ready(() => {
95
- this.jsPlumb = newInstance({
96
- container: canvas,
97
- connectionsDetachable: true,
98
- endpointStyle: {
99
- fill: 'green'
100
- },
101
- connector: CONNECTOR_DEFAULTS,
102
- connectionOverlays: OVERLAYS_DEFAULTS
103
- });
92
+ initializeJSPlumb(canvas: HTMLElement) {
93
+ this.jsPlumb = newInstance({
94
+ container: canvas,
95
+ connectionsDetachable: true,
96
+ endpointStyle: {
97
+ fill: 'green'
98
+ },
99
+ connector: CONNECTOR_DEFAULTS,
100
+ connectionOverlays: OVERLAYS_DEFAULTS
101
+ });
104
102
 
105
- // Bind to connection events
106
- this.jsPlumb.bind(EVENT_CONNECTION, (info) => {
107
- this.connectionDragging = false;
108
- this.notifyListeners(EVENT_CONNECTION, info);
109
- });
103
+ // Bind to connection events
104
+ this.jsPlumb.bind(EVENT_CONNECTION, (info) => {
105
+ this.connectionDragging = false;
106
+ this.notifyListeners(EVENT_CONNECTION, info);
107
+ });
110
108
 
111
- // Bind to connection drag events
112
- this.jsPlumb.bind(EVENT_CONNECTION_DRAG, (info) => {
113
- this.connectionDragging = true;
114
- this.notifyListeners(EVENT_CONNECTION_DRAG, info);
115
- });
109
+ // Bind to connection drag events
110
+ this.jsPlumb.bind(EVENT_CONNECTION_DRAG, (info) => {
111
+ this.connectionDragging = true;
112
+ this.notifyListeners(EVENT_CONNECTION_DRAG, info);
113
+ });
116
114
 
117
- this.jsPlumb.bind(EVENT_CONNECTION_ABORT, (info) => {
118
- this.connectionDragging = false;
119
- this.notifyListeners(EVENT_CONNECTION_ABORT, info);
120
- });
115
+ this.jsPlumb.bind(EVENT_CONNECTION_ABORT, (info) => {
116
+ this.connectionDragging = false;
117
+ this.notifyListeners(EVENT_CONNECTION_ABORT, info);
118
+ });
121
119
 
122
- this.jsPlumb.bind(EVENT_CONNECTION_DETACHED, (info) => {
123
- this.connectionDragging = false;
124
- this.notifyListeners(EVENT_CONNECTION_DETACHED, info);
125
- });
120
+ this.jsPlumb.bind(EVENT_CONNECTION_DETACHED, (info) => {
121
+ this.connectionDragging = false;
122
+ this.notifyListeners(EVENT_CONNECTION_DETACHED, info);
123
+ });
126
124
 
127
- this.jsPlumb.bind(EVENT_REVERT, (info) => {
128
- this.notifyListeners(EVENT_REVERT, info);
129
- });
125
+ this.jsPlumb.bind(EVENT_REVERT, (info) => {
126
+ this.notifyListeners(EVENT_REVERT, info);
127
+ });
130
128
 
131
- this.jsPlumb.bind(INTERCEPT_BEFORE_DROP, () => {
132
- // we always deny automatic connections
133
- return false;
134
- });
135
- this.jsPlumb.bind(INTERCEPT_BEFORE_DETACH, () => {});
129
+ this.jsPlumb.bind(INTERCEPT_BEFORE_DROP, () => {
130
+ // we always deny automatic connections
131
+ return false;
132
+ });
133
+ this.jsPlumb.bind(INTERCEPT_BEFORE_DETACH, () => {});
134
+ }
135
+
136
+ constructor(canvas: HTMLElement, editor: any) {
137
+ this.editor = editor;
138
+ ready(() => {
139
+ this.initializeJSPlumb(canvas);
136
140
  });
137
141
  }
138
142
 
@@ -159,12 +163,14 @@ export class Plumber {
159
163
 
160
164
  public makeTarget(uuid: string) {
161
165
  const element = document.getElementById(uuid);
162
- this.jsPlumb.addEndpoint(element, TARGET_DEFAULTS);
166
+ if (!element) return;
167
+ return this.jsPlumb.addEndpoint(element, TARGET_DEFAULTS);
163
168
  }
164
169
 
165
170
  public makeSource(uuid: string) {
166
171
  const element = document.getElementById(uuid);
167
- this.jsPlumb.addEndpoint(element, SOURCE_DEFAULTS);
172
+ if (!element) return;
173
+ return this.jsPlumb.addEndpoint(element, SOURCE_DEFAULTS);
168
174
  }
169
175
 
170
176
  // we'll process our pending connections, but we want to debounce this
@@ -180,27 +186,40 @@ export class Plumber {
180
186
  this.jsPlumb.batch(() => {
181
187
  this.pendingConnections.forEach((connection) => {
182
188
  const { scope, fromId, toId } = connection;
183
- const fromElement = document.getElementById(fromId);
184
- const toElement = document.getElementById(toId);
185
-
186
- // delete any existing endpoints
187
- this.jsPlumb.selectEndpoints({ source: fromId }).deleteAll();
188
-
189
- const source = this.jsPlumb.addEndpoint(fromElement, {
190
- ...SOURCE_DEFAULTS,
191
- endpoint: {
192
- ...SOURCE_DEFAULTS.endpoint,
193
- options: {
194
- ...SOURCE_DEFAULTS.endpoint.options,
195
- cssClass: 'plumb-source connected'
196
- }
197
- }
198
- });
199
189
 
200
- const target = this.jsPlumb.addEndpoint(toElement, TARGET_DEFAULTS);
190
+ // sources and targets must exist
191
+ const source = document.getElementById(fromId);
192
+ // const target = document.getElementById(toId);
193
+
194
+ this.revalidate([fromId, toId]);
195
+
196
+ // we need to find the source endpoint
197
+ const sourceEndpoint = this.jsPlumb
198
+ .getEndpoints(source)
199
+ ?.find((endpoint) =>
200
+ endpoint.elementId === fromId ? true : false
201
+ );
202
+
203
+ // update endpoint have connect css class
204
+ if (sourceEndpoint) {
205
+ sourceEndpoint.addClass('connected');
206
+ }
207
+
208
+ // each connection needs its own target endpoint
209
+ const targetEndpoint = this.makeTarget(toId);
210
+
211
+ if (!sourceEndpoint || !targetEndpoint) {
212
+ console.warn(
213
+ `Plumber: Cannot connect ${fromId} to ${toId}. Element(s) missing.`
214
+ );
215
+ return;
216
+ }
217
+
218
+ // delete connections
219
+ this.jsPlumb.select({ source, targetEndpoint }).deleteAll();
201
220
  this.jsPlumb.connect({
202
- source,
203
- target,
221
+ source: source,
222
+ target: targetEndpoint,
204
223
  connector: {
205
224
  ...CONNECTOR_DEFAULTS,
206
225
  options: { ...CONNECTOR_DEFAULTS.options, gap: [0, 5] }
@@ -212,7 +231,15 @@ export class Plumber {
212
231
  });
213
232
  this.pendingConnections = [];
214
233
  });
215
- }, 50);
234
+
235
+ // Force a repaint to ensure connections are positioned correctly
236
+ // especially after bulk updates or view switching
237
+ window.requestAnimationFrame(() => {
238
+ if (this.jsPlumb) {
239
+ this.jsPlumb.repaintEverything();
240
+ }
241
+ });
242
+ }, 0);
216
243
  }
217
244
 
218
245
  public connectIds(scope: string, fromId: string, toId: string) {
@@ -597,20 +624,44 @@ export class Plumber {
597
624
  });
598
625
  }
599
626
 
600
- public removeNodeConnections(nodeId: string) {
627
+ public reset() {
628
+ if (this.connectionWait) {
629
+ clearTimeout(this.connectionWait);
630
+ this.connectionWait = null;
631
+ }
632
+ this.pendingConnections = [];
633
+ this.jsPlumb.select().deleteAll();
634
+ this.jsPlumb._managedElements = {};
635
+ }
636
+
637
+ public forgetNode(nodeId: string) {
638
+ if (!this.jsPlumb) return;
639
+ const element = document.getElementById(nodeId);
640
+ if (!element) return;
641
+
642
+ this.jsPlumb.deleteConnectionsForElement(element);
643
+ this.jsPlumb.removeAllEndpoints(element);
644
+ this.jsPlumb.unmanage(element);
645
+ }
646
+
647
+ public removeNodeConnections(nodeId: string, exitIds?: string[]) {
601
648
  if (!this.jsPlumb) return;
602
649
 
603
650
  const inbound = this.jsPlumb.select({ target: nodeId });
604
- const exitIds =
651
+
652
+ // Use provided exitIds or try to find them in DOM (fallback)
653
+ const exits =
654
+ exitIds ||
605
655
  Array.from(
606
656
  document.getElementById(nodeId)?.querySelectorAll('.exit') || []
607
657
  ).map((exit) => {
608
658
  return exit.id;
609
- }) || [];
659
+ }) ||
660
+ [];
610
661
 
611
662
  inbound.deleteAll();
612
- this.jsPlumb.select({ source: exitIds }).deleteAll();
613
- this.jsPlumb.selectEndpoints({ source: exitIds }).deleteAll();
663
+ this.jsPlumb.select({ source: exits }).deleteAll();
664
+ this.jsPlumb.selectEndpoints({ source: exits }).deleteAll();
614
665
  }
615
666
 
616
667
  public removeExitConnection(exitId: string) {
@@ -627,13 +678,16 @@ export class Plumber {
627
678
  this.jsPlumb.deleteConnection(connection);
628
679
  });
629
680
 
630
- // Re-create the source endpoint (now without connection)
631
- this.jsPlumb.removeAllEndpoints(exitElement);
632
- this.makeSource(exitId);
633
-
634
681
  return connections.length > 0;
635
682
  }
636
683
 
684
+ public removeAllEndpoints(nodeId: string) {
685
+ if (!this.jsPlumb) return;
686
+ const element = document.getElementById(nodeId);
687
+ if (!element) return;
688
+ this.jsPlumb.removeAllEndpoints(element, true);
689
+ }
690
+
637
691
  /**
638
692
  * Set the removing state for an exit's connection
639
693
  * @param exitId The ID of the exit whose connections should be marked as removing
@@ -183,7 +183,6 @@ export class Simulator extends RapidElement {
183
183
  backdrop-filter: blur(10px);
184
184
  border-radius: 16px;
185
185
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
186
- pointer-events: all;
187
186
  }
188
187
  .option-btn {
189
188
  background: rgba(255, 255, 255, 0.1);
@@ -359,6 +358,10 @@ export class Simulator extends RapidElement {
359
358
  pointer-events: auto;
360
359
  }
361
360
 
361
+ .context-explorer.hidden {
362
+ pointer-events: none !important;
363
+ }
364
+
362
365
  .context-item {
363
366
  display: flex;
364
367
  align-items: flex-start;
@@ -1319,8 +1322,6 @@ export class Simulator extends RapidElement {
1319
1322
  const phoneWindow = this.shadowRoot.getElementById(
1320
1323
  'phone-window'
1321
1324
  ) as FloatingWindow;
1322
- // phoneWindow.hide();
1323
-
1324
1325
  phoneWindow.handleClose();
1325
1326
  this.isVisible = false;
1326
1327
  getStore().getState().setSimulatorActive(false);
@@ -1753,7 +1754,9 @@ export class Simulator extends RapidElement {
1753
1754
  >
1754
1755
  <div class="phone-simulator" style="${styleVars}">
1755
1756
  <div
1756
- class="context-explorer ${this.contextExplorerOpen ? 'open' : ''}"
1757
+ class="context-explorer ${this.contextExplorerOpen
1758
+ ? 'open'
1759
+ : ''} ${this.isVisible ? 'visible' : 'hidden'}"
1757
1760
  >
1758
1761
  <div class="context-explorer-scroll">
1759
1762
  ${this.context
@@ -1884,7 +1887,10 @@ export class Simulator extends RapidElement {
1884
1887
  </div>
1885
1888
  </div>
1886
1889
  </div>
1887
- <div class="option-pane">
1890
+ <div
1891
+ class="option-pane"
1892
+ style="pointer-events:${this.isVisible ? 'all' : 'none'}"
1893
+ >
1888
1894
  <button class="option-btn" @click=${this.handleClose} title="Close">
1889
1895
  <temba-icon name="x" size="1.5"></temba-icon>
1890
1896
  </button>
@@ -105,6 +105,7 @@ export interface AppState {
105
105
  languageNames: { [code: string]: Language };
106
106
  workspace: Workspace;
107
107
  isTranslating: boolean;
108
+ viewingRevision: boolean;
108
109
 
109
110
  dirtyDate: Date | null;
110
111
 
@@ -174,6 +175,7 @@ export const zustand = createStore<AppState>()(
174
175
  flowDefinition: null,
175
176
  flowInfo: null,
176
177
  isTranslating: false,
178
+ viewingRevision: false,
177
179
  dirtyDate: null,
178
180
  activity: null,
179
181
  simulatorActivity: null,
@@ -187,6 +189,7 @@ export const zustand = createStore<AppState>()(
187
189
  },
188
190
 
189
191
  fetchRevision: async (endpoint: string, id: string = null) => {
192
+ const viewingRevision = !!id && id !== 'latest';
190
193
  if (!id) {
191
194
  id = 'latest';
192
195
  }
@@ -198,7 +201,11 @@ export const zustand = createStore<AppState>()(
198
201
  throw new Error('Network response was not ok');
199
202
  }
200
203
  const data = (await response.json()) as FlowContents;
201
- set({ flowInfo: data.info, flowDefinition: data.definition });
204
+ set({
205
+ flowInfo: data.info,
206
+ flowDefinition: data.definition,
207
+ viewingRevision
208
+ });
202
209
  },
203
210
 
204
211
  fetchWorkspace: async (endpoint) => {
@@ -282,7 +289,11 @@ export const zustand = createStore<AppState>()(
282
289
  setFlowContents: (flow: FlowContents) => {
283
290
  set((state: AppState) => {
284
291
  const flowLang = flow.definition.language;
285
- state.flowDefinition = flow.definition;
292
+ // Clone to ensure mutable for sorting
293
+ state.flowDefinition = {
294
+ ...flow.definition,
295
+ nodes: [...(flow.definition.nodes || [])]
296
+ };
286
297
  state.flowInfo = flow.info;
287
298
  // Reset to the flow's default language when loading a new flow
288
299
  state.languageCode = flowLang;
@@ -835,7 +835,8 @@ describe('EditorNode', () => {
835
835
  makeTarget: stub(),
836
836
  makeSource: stub(),
837
837
  connectIds: stub(),
838
- removeExitConnection: stub()
838
+ removeExitConnection: stub(),
839
+ forgetNode: stub()
839
840
  };
840
841
  editorNode['plumber'] = mockPlumber;
841
842
  });
@@ -0,0 +1,134 @@
1
+ import { html, fixture, expect } from '@open-wc/testing';
2
+ import { Editor } from '../src/flow/Editor';
3
+ import { stub, restore, SinonStub } from 'sinon';
4
+
5
+ customElements.define('temba-flow-editor-revisions', Editor);
6
+
7
+ describe('Editor Revisions', () => {
8
+ let element: Editor;
9
+ let fetchStub: SinonStub;
10
+
11
+ beforeEach(async () => {
12
+ restore();
13
+ fetchStub = stub(window, 'fetch');
14
+ // Initialize without 'flow' attribute to prevent firstUpdated from calling getStore().getState()
15
+ element = await fixture(
16
+ html`<temba-flow-editor-revisions></temba-flow-editor-revisions>`
17
+ );
18
+ element.flow = 'test-flow';
19
+ });
20
+
21
+ afterEach(() => {
22
+ restore();
23
+ });
24
+
25
+ it('should exclude the most recent revision from the list', async () => {
26
+ const mockRevisions = [
27
+ {
28
+ id: 3,
29
+ created_on: '2023-01-03',
30
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
31
+ },
32
+ {
33
+ id: 2,
34
+ created_on: '2023-01-02',
35
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
36
+ },
37
+ {
38
+ id: 1,
39
+ created_on: '2023-01-01',
40
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
41
+ }
42
+ ];
43
+
44
+ // Mock the fetch response for the revisions list
45
+ const mockResponse = new Response(
46
+ JSON.stringify({ results: mockRevisions }),
47
+ {
48
+ status: 200,
49
+ headers: { 'Content-Type': 'application/json' }
50
+ }
51
+ );
52
+
53
+ fetchStub.resolves(mockResponse);
54
+
55
+ // Call fetchRevisions (private)
56
+ // Note: fetchRevisions calls fetchResults -> fetchResultsPage -> fetch
57
+ await (element as any).fetchRevisions();
58
+
59
+ const revisions = (element as any).revisions;
60
+ expect(revisions.length).to.equal(2);
61
+ expect(revisions[0].id).to.equal(2);
62
+ expect(revisions[1].id).to.equal(1);
63
+ });
64
+
65
+ it('should handle empty revisions list', async () => {
66
+ const mockResponse = new Response(JSON.stringify({ results: [] }), {
67
+ status: 200
68
+ });
69
+ fetchStub.resolves(mockResponse);
70
+
71
+ await (element as any).fetchRevisions();
72
+ const revisions = (element as any).revisions;
73
+ expect(revisions.length).to.equal(0);
74
+ });
75
+
76
+ it('should handle single revision in list', async () => {
77
+ const mockRevisions = [
78
+ {
79
+ id: 1,
80
+ created_on: '2023-01-01',
81
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
82
+ }
83
+ ];
84
+ const mockResponse = new Response(
85
+ JSON.stringify({ results: mockRevisions }),
86
+ { status: 200 }
87
+ );
88
+ fetchStub.resolves(mockResponse);
89
+
90
+ await (element as any).fetchRevisions();
91
+ const revisions = (element as any).revisions;
92
+ expect(revisions.length).to.equal(0);
93
+ });
94
+
95
+ it('should have purple color for revisions tab and blue for selected item', async () => {
96
+ // Force revisions window to show
97
+ (element as any).revisionsWindowHidden = false;
98
+ (element as any).localizationWindowHidden = true;
99
+
100
+ // Mock revisions so we can see list items
101
+ const mockRevisions = [
102
+ {
103
+ id: 2,
104
+ created_on: '2023-01-02',
105
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
106
+ },
107
+ {
108
+ id: 1,
109
+ created_on: '2023-01-01',
110
+ user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
111
+ }
112
+ ];
113
+ (element as any).revisions = mockRevisions;
114
+ (element as any).viewingRevision = mockRevisions[0]; // Select the first one
115
+
116
+ await element.requestUpdate();
117
+
118
+ // Check tab color
119
+ const tab = element.querySelector('#revisions-tab');
120
+ expect(tab).to.exist;
121
+ expect(tab.getAttribute('color')).to.equal('rgb(142, 94, 167)');
122
+
123
+ // Check selected item styles
124
+ const selectedItem = element.querySelector(
125
+ '.revision-item.selected'
126
+ ) as HTMLElement;
127
+ expect(selectedItem).to.exist;
128
+
129
+ // We need to check inline styles because they are set in the template
130
+ const style = selectedItem.getAttribute('style');
131
+ expect(style).to.contain('#f0f6ff'); // Blue background
132
+ expect(style).to.contain('#a4cafe'); // Blue border
133
+ });
134
+ });
@@ -653,11 +653,14 @@ describe('Editor', () => {
653
653
 
654
654
  await editor.updateComplete;
655
655
 
656
- // node-2 should be first (top: 100 < top: 200)
657
- const flowNodes = editor.querySelectorAll('temba-flow-node');
658
- expect(flowNodes[0].getAttribute('uuid')).to.equal('node-2');
659
- expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
660
- expect(flowNodes[1].classList.contains('flow-start')).to.be.false;
656
+ // node-2 is at top (top: 100 < top: 200) so it should be flow-start
657
+ const node1 = editor.querySelector('temba-flow-node[uuid="node-1"]');
658
+ const node2 = editor.querySelector('temba-flow-node[uuid="node-2"]');
659
+
660
+ expect(node2).to.exist;
661
+ expect(node1).to.exist;
662
+ expect(node2.classList.contains('flow-start')).to.be.true;
663
+ expect(node1.classList.contains('flow-start')).to.be.false;
661
664
 
662
665
  // move node-1 to the top
663
666
  zustand.getState().updateCanvasPositions({
@@ -720,7 +723,7 @@ describe('Editor', () => {
720
723
 
721
724
  await editor.updateComplete;
722
725
 
723
- let flowNodes = editor.querySelectorAll('temba-flow-node');
726
+ const flowNodes = editor.querySelectorAll('temba-flow-node');
724
727
  expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
725
728
 
726
729
  // add a new node at the top
@@ -741,10 +744,13 @@ describe('Editor', () => {
741
744
  await editor.updateComplete;
742
745
 
743
746
  // new node should now be the flow-start
744
- flowNodes = editor.querySelectorAll('temba-flow-node');
745
- expect(flowNodes[0].getAttribute('uuid')).to.equal('node-2');
746
- expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
747
- expect(flowNodes[1].classList.contains('flow-start')).to.be.false;
747
+ const node1 = editor.querySelector('temba-flow-node[uuid="node-1"]');
748
+ const node2 = editor.querySelector('temba-flow-node[uuid="node-2"]');
749
+
750
+ expect(node2).to.exist;
751
+ expect(node1).to.exist;
752
+ expect(node2.classList.contains('flow-start')).to.be.true;
753
+ expect(node1.classList.contains('flow-start')).to.be.false;
748
754
  });
749
755
 
750
756
  it('should handle flow-start when first node is removed', async () => {
@@ -29,7 +29,14 @@ describe('Plumber - Connection Management', () => {
29
29
  removeClass: stub(),
30
30
  batch: stub().callsFake((fn: any) => fn()),
31
31
  addEndpoint: stub().returns({}),
32
+ revalidate: stub(),
32
33
  connect: stub(),
34
+ getEndpoints: stub().returns([
35
+ { elementId: 'test-from', addClass: stub() }
36
+ ]),
37
+ select: stub().returns({
38
+ deleteAll: stub()
39
+ }),
33
40
  selectEndpoints: stub().returns({
34
41
  deleteAll: stub()
35
42
  }),
@@ -131,7 +138,6 @@ describe('Plumber - Connection Management', () => {
131
138
  expect(result).to.be.true;
132
139
  expect((plumber as any).jsPlumb.deleteConnection).to.have.been
133
140
  .calledTwice;
134
- expect((plumber as any).jsPlumb.removeAllEndpoints).to.have.been.called;
135
141
  });
136
142
 
137
143
  it('returns false when no connections exist', () => {
@@ -31,6 +31,12 @@ describe('Plumber', () => {
31
31
  batch: stub().callsFake((fn) => fn()),
32
32
  addEndpoint: stub().returns({}),
33
33
  connect: stub(),
34
+ getEndpoints: stub().returns([
35
+ { elementId: 'test-from', addClass: stub() }
36
+ ]),
37
+ select: stub().returns({
38
+ deleteAll: stub()
39
+ }),
34
40
  selectEndpoints: stub().returns({
35
41
  deleteAll: stub()
36
42
  }),