@nyaruka/temba-components 0.141.1 → 0.142.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +849 -655
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +3 -1
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/display/Button.js +2 -2
- package/out-tsc/src/display/Button.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +24 -1
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +7 -2
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +654 -66
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +8 -5
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +40 -28
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +2 -1
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/reflow.js +393 -0
- package/out-tsc/src/flow/reflow.js.map +1 -0
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +18 -3
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/Compose.js +5 -0
- package/out-tsc/src/form/Compose.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +1 -3
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +2 -0
- package/out-tsc/src/layout/Dialog.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +39 -19
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/test/temba-canvas-menu.test.js +44 -0
- package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +25 -0
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-zoom.test.js +491 -0
- package/out-tsc/test/temba-flow-editor-zoom.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor.test.js +145 -1
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-node-drag.test.js +123 -0
- package/out-tsc/test/temba-flow-node-drag.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +31 -0
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-flow-reflow.test.js +472 -0
- package/out-tsc/test/temba-flow-reflow.test.js.map +1 -0
- package/out-tsc/test/temba-sortable-list.test.js +93 -0
- package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
- package/screenshots/truth/actions/enter_flow/editor/basic-flow.png +0 -0
- package/screenshots/truth/actions/enter_flow/editor/long-flow-name.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.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/send_broadcast/editor/contacts-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/set_contact_channel/editor/sms-channel.png +0 -0
- package/screenshots/truth/actions/set_contact_channel/editor/whatsapp-channel.png +0 -0
- package/screenshots/truth/actions/set_contact_field/editor/clear-value.png +0 -0
- package/screenshots/truth/actions/set_contact_field/editor/set-value.png +0 -0
- package/screenshots/truth/actions/set_contact_language/editor/english.png +0 -0
- package/screenshots/truth/actions/set_contact_language/editor/french.png +0 -0
- package/screenshots/truth/actions/set_contact_status/editor/active.png +0 -0
- package/screenshots/truth/actions/set_contact_status/editor/archived.png +0 -0
- package/screenshots/truth/actions/set_contact_status/editor/blocked.png +0 -0
- package/screenshots/truth/actions/set_run_result/editor/expression-value.png +0 -0
- package/screenshots/truth/actions/set_run_result/editor/with-category.png +0 -0
- package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
- package/screenshots/truth/list/fields-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dragging.png +0 -0
- package/screenshots/truth/modax/simple.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/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_menu/editor/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/src/Icons.ts +3 -1
- package/src/display/Button.ts +2 -2
- package/src/display/FloatingTab.ts +1 -1
- package/src/flow/CanvasMenu.ts +28 -3
- package/src/flow/CanvasNode.ts +7 -2
- package/src/flow/Editor.ts +755 -75
- package/src/flow/NodeEditor.ts +8 -4
- package/src/flow/Plumber.ts +65 -35
- package/src/flow/actions/send_msg.ts +2 -1
- package/src/flow/nodes/wait_for_response.ts +1 -1
- package/src/flow/reflow.ts +534 -0
- package/src/flow/types.ts +1 -0
- package/src/flow/utils.ts +19 -3
- package/src/form/Compose.ts +5 -0
- package/src/form/FieldRenderer.ts +1 -3
- package/src/layout/Dialog.ts +2 -0
- package/src/list/SortableList.ts +40 -19
- package/static/svg/index.svg +1 -1
- package/static/svg/work/traced/expand-06.svg +1 -0
- package/static/svg/work/used/expand-06.svg +3 -0
- package/test/temba-canvas-menu.test.ts +55 -0
- package/test/temba-flow-collision.test.ts +31 -0
- package/test/temba-flow-editor-zoom.test.ts +583 -0
- package/test/temba-flow-editor.test.ts +187 -1
- package/test/temba-flow-node-drag.test.ts +171 -0
- package/test/temba-flow-plumber.test.ts +38 -0
- package/test/temba-flow-reflow.test.ts +703 -0
- package/test/temba-sortable-list.test.ts +120 -0
- package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
- package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
- package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
- package/screenshots/truth/compose/attachments-with-failures.png +0 -0
- package/screenshots/truth/compose/attachments-with-files-and-failures.png +0 -0
- package/screenshots/truth/contacts/tickets-assignment.png +0 -0
- package/screenshots/truth/contacts/tickets.png +0 -0
- package/screenshots/truth/flow/editor-basic.png +0 -0
- package/screenshots/truth/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
- package/screenshots/truth/omnibox/selected.png +0 -0
- package/screenshots/truth/select/enabled-multi-selection.png +0 -0
- package/screenshots/truth/select/endpoint-initial-value-updated.png +0 -0
- package/screenshots/truth/select/endpoint-initial-value.png +0 -0
- package/screenshots/truth/select/initial-value.png +0 -0
- package/screenshots/truth/select/multi-reorder-final.png +0 -0
- package/screenshots/truth/select/multi-reorder-initial.png +0 -0
- package/screenshots/truth/select/selected-multi-test.png +0 -0
- package/screenshots/truth/select/value-initial.png +0 -0
- package/screenshots/truth/wait-for-response/rules-editor.png +0 -0
- package/screenshots/truth/wait-for-response/timeout-editor-unchecked.png +0 -0
- package/screenshots/truth/wait-for-response/timeout-editor.png +0 -0
- package/screenshots/truth/webchat/connecting-state.png +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.311 2.307 C 2.023 2.517,2.000 2.748,2.000 5.410 C 2.000 8.014,2.029 8.318,2.305 8.623 C 2.711 9.072,3.430 9.063,3.751 8.604 C 3.932 8.346,4.000 7.853,4.000 6.807 L 4.000 5.365 5.803 7.163 C 7.706 9.060,8.137 9.285,8.695 8.667 C 9.243 8.062,9.042 7.688,7.163 5.803 L 5.365 4.000 6.807 4.000 C 8.466 4.000,8.960 3.783,8.960 3.056 C 8.960 2.162,8.670 2.080,5.498 2.080 C 3.266 2.080,2.552 2.131,2.311 2.307 M15.291 2.331 C 15.153 2.470,15.040 2.790,15.040 3.042 C 15.040 3.785,15.524 4.000,17.193 4.000 L 18.635 4.000 16.837 5.803 C 14.940 7.706,14.715 8.137,15.333 8.695 C 15.938 9.243,16.312 9.042,18.197 7.163 L 20.000 5.365 20.000 6.807 C 20.000 7.853,20.068 8.346,20.249 8.604 C 20.570 9.063,21.289 9.072,21.695 8.623 C 21.971 8.318,22.000 8.014,22.000 5.410 C 22.000 2.748,21.977 2.517,21.689 2.307 C 21.211 1.957,15.644 1.979,15.291 2.331 M2.338 15.285 C 1.965 15.658,1.891 16.358,1.948 19.004 C 1.992 21.075,2.042 21.471,2.286 21.715 C 2.538 21.967,2.901 22.000,5.429 22.000 C 8.014 22.000,8.318 21.971,8.623 21.695 C 9.072 21.289,9.063 20.570,8.604 20.249 C 8.346 20.068,7.853 20.000,6.807 20.000 L 5.365 20.000 7.163 18.197 C 9.060 16.294,9.285 15.863,8.667 15.305 C 8.062 14.757,7.688 14.958,5.803 16.837 L 4.000 18.635 4.000 17.193 C 4.000 15.524,3.785 15.040,3.042 15.040 C 2.790 15.040,2.473 15.150,2.338 15.285 M15.305 15.333 C 14.757 15.938,14.958 16.312,16.837 18.197 L 18.635 20.000 17.193 20.000 C 16.147 20.000,15.654 20.068,15.396 20.249 C 14.938 20.570,14.928 21.289,15.376 21.695 C 15.672 21.963,16.023 22.006,18.276 22.051 C 21.112 22.108,21.621 22.001,21.918 21.286 C 22.039 20.992,22.085 19.965,22.051 18.276 C 22.006 16.023,21.963 15.672,21.695 15.376 C 21.289 14.928,20.570 14.938,20.249 15.396 C 20.068 15.654,20.000 16.147,20.000 17.193 L 20.000 18.635 18.197 16.837 C 16.294 14.940,15.863 14.715,15.305 15.333 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M16 8L21 3M21 3H16M21 3V8M8 8L3 3M3 3L3 8M3 3L8 3M8 16L3 21M3 21H8M3 21L3 16M16 16L21 21M21 21V16M21 21H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
</svg>
|
|
@@ -107,6 +107,61 @@ describe('temba-canvas-menu', () => {
|
|
|
107
107
|
expect(menu.open).to.be.false;
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
it('shows reflow option when showReflow is true', async () => {
|
|
111
|
+
const menu = await createCanvasMenu();
|
|
112
|
+
menu.show(100, 100, { x: 50, y: 50 }, true, true);
|
|
113
|
+
await menu.updateComplete;
|
|
114
|
+
|
|
115
|
+
const menuItems = menu.shadowRoot?.querySelectorAll('.menu-item');
|
|
116
|
+
expect(menuItems?.length).to.equal(4);
|
|
117
|
+
|
|
118
|
+
const titles = Array.from(menuItems || []).map(
|
|
119
|
+
(item) => item.querySelector('.menu-item-title')?.textContent
|
|
120
|
+
);
|
|
121
|
+
expect(titles).to.deep.equal([
|
|
122
|
+
'Add Action',
|
|
123
|
+
'Add Split',
|
|
124
|
+
'Add Sticky Note',
|
|
125
|
+
'Reflow'
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('fires reflow selection event when reflow is clicked', async () => {
|
|
130
|
+
const menu = await createCanvasMenu();
|
|
131
|
+
menu.show(100, 100, { x: 50, y: 50 }, true, true);
|
|
132
|
+
await menu.updateComplete;
|
|
133
|
+
|
|
134
|
+
let selectionDetail = null;
|
|
135
|
+
menu.addEventListener('temba-selection', (event: any) => {
|
|
136
|
+
selectionDetail = event.detail;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const menuItems = menu.shadowRoot?.querySelectorAll('.menu-item');
|
|
140
|
+
const reflowItem = menuItems?.[3] as HTMLElement;
|
|
141
|
+
reflowItem.click();
|
|
142
|
+
await menu.updateComplete;
|
|
143
|
+
|
|
144
|
+
expect(selectionDetail).to.deep.equal({
|
|
145
|
+
action: 'reflow',
|
|
146
|
+
position: { x: 50, y: 50 }
|
|
147
|
+
});
|
|
148
|
+
expect(menu.open).to.be.false;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('hides reflow option by default', async () => {
|
|
152
|
+
const menu = await createCanvasMenu();
|
|
153
|
+
menu.show(100, 100, { x: 50, y: 50 });
|
|
154
|
+
await menu.updateComplete;
|
|
155
|
+
|
|
156
|
+
const menuItems = menu.shadowRoot?.querySelectorAll('.menu-item');
|
|
157
|
+
expect(menuItems?.length).to.equal(3);
|
|
158
|
+
|
|
159
|
+
const titles = Array.from(menuItems || []).map(
|
|
160
|
+
(item) => item.querySelector('.menu-item-title')?.textContent
|
|
161
|
+
);
|
|
162
|
+
expect(titles).to.not.include('Reflow');
|
|
163
|
+
});
|
|
164
|
+
|
|
110
165
|
it('adjusts position to stay within viewport bounds', async () => {
|
|
111
166
|
const menu = await createCanvasMenu();
|
|
112
167
|
|
|
@@ -37,6 +37,37 @@ describe('Collision Detection Utilities', () => {
|
|
|
37
37
|
|
|
38
38
|
document.body.removeChild(mockElement);
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
it('uses layout-space dimensions unaffected by ancestor CSS transform', () => {
|
|
42
|
+
// Simulate zoom: ancestor has transform: scale(0.5)
|
|
43
|
+
const container = document.createElement('div');
|
|
44
|
+
container.style.transform = 'scale(0.5)';
|
|
45
|
+
container.style.transformOrigin = '0 0';
|
|
46
|
+
document.body.appendChild(container);
|
|
47
|
+
|
|
48
|
+
const mockElement = document.createElement('div');
|
|
49
|
+
mockElement.id = 'zoom-test-node';
|
|
50
|
+
mockElement.style.width = '200px';
|
|
51
|
+
mockElement.style.height = '150px';
|
|
52
|
+
container.appendChild(mockElement);
|
|
53
|
+
|
|
54
|
+
const position = { left: 100, top: 200 };
|
|
55
|
+
const bounds = getNodeBounds('zoom-test-node', position, mockElement);
|
|
56
|
+
|
|
57
|
+
// getNodeBounds uses offsetWidth/offsetHeight which are layout-space
|
|
58
|
+
expect(bounds).to.not.be.null;
|
|
59
|
+
expect(bounds!.width).to.equal(200);
|
|
60
|
+
expect(bounds!.height).to.equal(150);
|
|
61
|
+
expect(bounds!.right).to.equal(300); // left + layout width
|
|
62
|
+
expect(bounds!.bottom).to.equal(350); // top + layout height
|
|
63
|
+
|
|
64
|
+
// Verify that getBoundingClientRect WOULD return scaled values
|
|
65
|
+
const rect = mockElement.getBoundingClientRect();
|
|
66
|
+
expect(rect.width).to.equal(100); // 200 * 0.5 scale
|
|
67
|
+
expect(rect.height).to.equal(75); // 150 * 0.5 scale
|
|
68
|
+
|
|
69
|
+
container.remove();
|
|
70
|
+
});
|
|
40
71
|
});
|
|
41
72
|
|
|
42
73
|
describe('nodesOverlap', () => {
|
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { Editor } from '../src/flow/Editor';
|
|
3
|
+
import { stub, restore, spy } from 'sinon';
|
|
4
|
+
|
|
5
|
+
customElements.define('temba-flow-editor-zoom', Editor);
|
|
6
|
+
|
|
7
|
+
/** Create an Editor instance with a mock plumber, suitable for unit-testing
|
|
8
|
+
* zoom methods without needing a full flow definition. */
|
|
9
|
+
const createEditorWithMockPlumber = (): Editor => {
|
|
10
|
+
const editor = new Editor();
|
|
11
|
+
(editor as any).plumber = {
|
|
12
|
+
zoom: 1,
|
|
13
|
+
repaintEverything: stub()
|
|
14
|
+
};
|
|
15
|
+
return editor;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('Editor Zoom', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
restore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
restore();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// --- A. setZoom state management ---
|
|
28
|
+
|
|
29
|
+
describe('setZoom', () => {
|
|
30
|
+
it('clamps to minimum of 0.1', () => {
|
|
31
|
+
const editor = createEditorWithMockPlumber();
|
|
32
|
+
(editor as any).zoom = 0.5;
|
|
33
|
+
(editor as any).setZoom(0.02);
|
|
34
|
+
expect((editor as any).zoom).to.equal(0.1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('clamps to maximum of 1.0', () => {
|
|
38
|
+
const editor = createEditorWithMockPlumber();
|
|
39
|
+
(editor as any).zoom = 0.5;
|
|
40
|
+
(editor as any).setZoom(1.5);
|
|
41
|
+
expect((editor as any).zoom).to.equal(1.0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rounds to 2 decimal places', () => {
|
|
45
|
+
const editor = createEditorWithMockPlumber();
|
|
46
|
+
(editor as any).zoom = 0.5;
|
|
47
|
+
(editor as any).setZoom(0.333);
|
|
48
|
+
expect((editor as any).zoom).to.equal(0.33);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rounds 0.555 to 0.56', () => {
|
|
52
|
+
const editor = createEditorWithMockPlumber();
|
|
53
|
+
(editor as any).zoom = 0.5;
|
|
54
|
+
(editor as any).setZoom(0.555);
|
|
55
|
+
expect((editor as any).zoom).to.equal(0.56);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('is a no-op when clamped value equals current zoom', () => {
|
|
59
|
+
const editor = createEditorWithMockPlumber();
|
|
60
|
+
(editor as any).zoom = 0.5;
|
|
61
|
+
const rafSpy = spy(window, 'requestAnimationFrame');
|
|
62
|
+
(editor as any).setZoom(0.5);
|
|
63
|
+
expect((editor as any).zoom).to.equal(0.5);
|
|
64
|
+
expect(rafSpy).to.not.have.been.called;
|
|
65
|
+
rafSpy.restore();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('syncs plumber.zoom', () => {
|
|
69
|
+
const editor = createEditorWithMockPlumber();
|
|
70
|
+
(editor as any).zoom = 0.5;
|
|
71
|
+
(editor as any).setZoom(0.75);
|
|
72
|
+
expect((editor as any).plumber.zoom).to.equal(0.75);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('clears zoomFitted flag', () => {
|
|
76
|
+
const editor = createEditorWithMockPlumber();
|
|
77
|
+
(editor as any).zoom = 0.5;
|
|
78
|
+
(editor as any).zoomFitted = true;
|
|
79
|
+
(editor as any).setZoom(0.8);
|
|
80
|
+
expect((editor as any).zoomFitted).to.be.false;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('preserves viewport center point when center is provided', () => {
|
|
84
|
+
const editor = createEditorWithMockPlumber();
|
|
85
|
+
(editor as any).zoom = 1.0;
|
|
86
|
+
|
|
87
|
+
// Create mock #editor element with known geometry
|
|
88
|
+
const mockEditor = {
|
|
89
|
+
scrollLeft: 100,
|
|
90
|
+
scrollTop: 200,
|
|
91
|
+
getBoundingClientRect: () => ({
|
|
92
|
+
left: 50,
|
|
93
|
+
top: 50,
|
|
94
|
+
width: 800,
|
|
95
|
+
height: 600
|
|
96
|
+
})
|
|
97
|
+
};
|
|
98
|
+
stub(editor, 'querySelector').returns(mockEditor as any);
|
|
99
|
+
|
|
100
|
+
// Stub rAF to invoke callback synchronously
|
|
101
|
+
const rafStub = stub(window, 'requestAnimationFrame').callsFake(
|
|
102
|
+
(cb: FrameRequestCallback) => {
|
|
103
|
+
cb(0);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const center = { clientX: 450, clientY: 350 };
|
|
109
|
+
(editor as any).setZoom(0.5, center);
|
|
110
|
+
|
|
111
|
+
// Verify scroll math:
|
|
112
|
+
// ox = 450 - 50 = 400, oy = 350 - 50 = 300
|
|
113
|
+
// cx = (100 + 400) / 1.0 = 500, cy = (200 + 300) / 1.0 = 500
|
|
114
|
+
// newScrollLeft = 500 * 0.5 - 400 = -150
|
|
115
|
+
// newScrollTop = 500 * 0.5 - 300 = -50
|
|
116
|
+
expect(mockEditor.scrollLeft).to.equal(-150);
|
|
117
|
+
expect(mockEditor.scrollTop).to.equal(-50);
|
|
118
|
+
|
|
119
|
+
rafStub.restore();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// --- B. Convenience methods ---
|
|
124
|
+
|
|
125
|
+
describe('zoomIn', () => {
|
|
126
|
+
it('increments zoom by 0.05', () => {
|
|
127
|
+
const editor = createEditorWithMockPlumber();
|
|
128
|
+
(editor as any).zoom = 0.5;
|
|
129
|
+
(editor as any).zoomIn();
|
|
130
|
+
expect((editor as any).zoom).to.equal(0.55);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('stays at 1.0 when already at maximum', () => {
|
|
134
|
+
const editor = createEditorWithMockPlumber();
|
|
135
|
+
(editor as any).zoom = 1.0;
|
|
136
|
+
(editor as any).zoomIn();
|
|
137
|
+
expect((editor as any).zoom).to.equal(1.0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('zoomOut', () => {
|
|
142
|
+
it('decrements zoom by 0.05', () => {
|
|
143
|
+
const editor = createEditorWithMockPlumber();
|
|
144
|
+
(editor as any).zoom = 0.5;
|
|
145
|
+
(editor as any).zoomOut();
|
|
146
|
+
expect((editor as any).zoom).to.equal(0.45);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('stays at 0.1 when already at minimum', () => {
|
|
150
|
+
const editor = createEditorWithMockPlumber();
|
|
151
|
+
(editor as any).zoom = 0.1;
|
|
152
|
+
(editor as any).zoomOut();
|
|
153
|
+
expect((editor as any).zoom).to.equal(0.1);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('zoomToFull', () => {
|
|
158
|
+
it('resets zoom to 1.0', () => {
|
|
159
|
+
const editor = createEditorWithMockPlumber();
|
|
160
|
+
(editor as any).zoom = 0.4;
|
|
161
|
+
(editor as any).zoomToFull();
|
|
162
|
+
expect((editor as any).zoom).to.equal(1.0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --- C. zoomToFit ---
|
|
167
|
+
|
|
168
|
+
describe('zoomToFit', () => {
|
|
169
|
+
it('returns early when no definition', () => {
|
|
170
|
+
const editor = createEditorWithMockPlumber();
|
|
171
|
+
(editor as any).definition = null;
|
|
172
|
+
(editor as any).zoomToFit();
|
|
173
|
+
expect((editor as any).zoom).to.equal(1.0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('returns early when definition has zero nodes', () => {
|
|
177
|
+
const editor = createEditorWithMockPlumber();
|
|
178
|
+
(editor as any).definition = { nodes: [], _ui: { nodes: {} } };
|
|
179
|
+
(editor as any).zoomToFit();
|
|
180
|
+
expect((editor as any).zoom).to.equal(1.0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('sets zoomFitted to true', () => {
|
|
184
|
+
const editor = createEditorWithMockPlumber();
|
|
185
|
+
|
|
186
|
+
// Create mock definition with nodes spread wide enough for zoom < 1.0
|
|
187
|
+
const node1uuid = 'node-1-uuid';
|
|
188
|
+
const node2uuid = 'node-2-uuid';
|
|
189
|
+
(editor as any).definition = {
|
|
190
|
+
nodes: [{ uuid: node1uuid }, { uuid: node2uuid }],
|
|
191
|
+
_ui: {
|
|
192
|
+
nodes: {
|
|
193
|
+
[node1uuid]: { position: { left: 0, top: 0 } },
|
|
194
|
+
[node2uuid]: { position: { left: 2000, top: 1500 } }
|
|
195
|
+
},
|
|
196
|
+
stickies: {}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Create mock node elements
|
|
201
|
+
const el1 = document.createElement('div');
|
|
202
|
+
el1.id = node1uuid;
|
|
203
|
+
el1.style.width = '200px';
|
|
204
|
+
el1.style.height = '100px';
|
|
205
|
+
document.body.appendChild(el1);
|
|
206
|
+
|
|
207
|
+
const el2 = document.createElement('div');
|
|
208
|
+
el2.id = node2uuid;
|
|
209
|
+
el2.style.width = '200px';
|
|
210
|
+
el2.style.height = '100px';
|
|
211
|
+
document.body.appendChild(el2);
|
|
212
|
+
|
|
213
|
+
// Create mock #editor element
|
|
214
|
+
const mockEditor = document.createElement('div');
|
|
215
|
+
mockEditor.id = 'mock-editor-for-zoom';
|
|
216
|
+
Object.defineProperty(mockEditor, 'clientWidth', { value: 800 });
|
|
217
|
+
Object.defineProperty(mockEditor, 'clientHeight', { value: 600 });
|
|
218
|
+
mockEditor.scrollLeft = 0;
|
|
219
|
+
mockEditor.scrollTop = 0;
|
|
220
|
+
|
|
221
|
+
stub(editor, 'querySelector').callsFake((selector: string) => {
|
|
222
|
+
if (selector === '#editor') return mockEditor;
|
|
223
|
+
if (selector.includes(node1uuid)) return el1;
|
|
224
|
+
if (selector.includes(node2uuid)) return el2;
|
|
225
|
+
return null;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
stub(window, 'requestAnimationFrame').callsFake(
|
|
229
|
+
(cb: FrameRequestCallback) => {
|
|
230
|
+
cb(0);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
(editor as any).zoomToFit();
|
|
236
|
+
expect((editor as any).zoomFitted).to.be.true;
|
|
237
|
+
|
|
238
|
+
el1.remove();
|
|
239
|
+
el2.remove();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('caps zoom at 1.0 when nodes fit easily', () => {
|
|
243
|
+
const editor = createEditorWithMockPlumber();
|
|
244
|
+
|
|
245
|
+
// Tiny content that fits easily
|
|
246
|
+
const nodeUuid = 'small-node';
|
|
247
|
+
(editor as any).definition = {
|
|
248
|
+
nodes: [{ uuid: nodeUuid }],
|
|
249
|
+
_ui: {
|
|
250
|
+
nodes: {
|
|
251
|
+
[nodeUuid]: { position: { left: 100, top: 100 } }
|
|
252
|
+
},
|
|
253
|
+
stickies: {}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const el = document.createElement('div');
|
|
258
|
+
el.id = nodeUuid;
|
|
259
|
+
el.style.width = '50px';
|
|
260
|
+
el.style.height = '30px';
|
|
261
|
+
document.body.appendChild(el);
|
|
262
|
+
|
|
263
|
+
const mockEditor = document.createElement('div');
|
|
264
|
+
Object.defineProperty(mockEditor, 'clientWidth', { value: 800 });
|
|
265
|
+
Object.defineProperty(mockEditor, 'clientHeight', { value: 600 });
|
|
266
|
+
mockEditor.scrollLeft = 0;
|
|
267
|
+
mockEditor.scrollTop = 0;
|
|
268
|
+
|
|
269
|
+
stub(editor, 'querySelector').callsFake((selector: string) => {
|
|
270
|
+
if (selector === '#editor') return mockEditor;
|
|
271
|
+
if (selector.includes(nodeUuid)) return el;
|
|
272
|
+
return null;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
stub(window, 'requestAnimationFrame').callsFake(
|
|
276
|
+
(cb: FrameRequestCallback) => {
|
|
277
|
+
cb(0);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
(editor as any).zoomToFit();
|
|
283
|
+
expect((editor as any).zoom).to.equal(1.0);
|
|
284
|
+
|
|
285
|
+
el.remove();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('rounds zoom to nearest 0.05', () => {
|
|
289
|
+
const editor = createEditorWithMockPlumber();
|
|
290
|
+
|
|
291
|
+
// Create nodes that produce a zoom that's not a multiple of 0.05
|
|
292
|
+
const node1uuid = 'fit-node-1';
|
|
293
|
+
const node2uuid = 'fit-node-2';
|
|
294
|
+
(editor as any).definition = {
|
|
295
|
+
nodes: [{ uuid: node1uuid }, { uuid: node2uuid }],
|
|
296
|
+
_ui: {
|
|
297
|
+
nodes: {
|
|
298
|
+
[node1uuid]: { position: { left: 0, top: 0 } },
|
|
299
|
+
[node2uuid]: { position: { left: 3000, top: 2000 } }
|
|
300
|
+
},
|
|
301
|
+
stickies: {}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const el1 = document.createElement('div');
|
|
306
|
+
el1.id = node1uuid;
|
|
307
|
+
el1.style.width = '200px';
|
|
308
|
+
el1.style.height = '100px';
|
|
309
|
+
document.body.appendChild(el1);
|
|
310
|
+
|
|
311
|
+
const el2 = document.createElement('div');
|
|
312
|
+
el2.id = node2uuid;
|
|
313
|
+
el2.style.width = '200px';
|
|
314
|
+
el2.style.height = '100px';
|
|
315
|
+
document.body.appendChild(el2);
|
|
316
|
+
|
|
317
|
+
const mockEditor = document.createElement('div');
|
|
318
|
+
Object.defineProperty(mockEditor, 'clientWidth', { value: 800 });
|
|
319
|
+
Object.defineProperty(mockEditor, 'clientHeight', { value: 600 });
|
|
320
|
+
mockEditor.scrollLeft = 0;
|
|
321
|
+
mockEditor.scrollTop = 0;
|
|
322
|
+
|
|
323
|
+
stub(editor, 'querySelector').callsFake((selector: string) => {
|
|
324
|
+
if (selector === '#editor') return mockEditor;
|
|
325
|
+
if (selector.includes(node1uuid)) return el1;
|
|
326
|
+
if (selector.includes(node2uuid)) return el2;
|
|
327
|
+
return null;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
stub(window, 'requestAnimationFrame').callsFake(
|
|
331
|
+
(cb: FrameRequestCallback) => {
|
|
332
|
+
cb(0);
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
(editor as any).zoomToFit();
|
|
338
|
+
|
|
339
|
+
const zoom = (editor as any).zoom;
|
|
340
|
+
// Zoom should be a multiple of 0.05 (within float tolerance)
|
|
341
|
+
const remainder = Math.round((zoom % 0.05) * 1000) / 1000;
|
|
342
|
+
expect(remainder === 0 || remainder === 0.05).to.be.true;
|
|
343
|
+
|
|
344
|
+
el1.remove();
|
|
345
|
+
el2.remove();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// --- D. handleWheel ---
|
|
350
|
+
|
|
351
|
+
describe('handleWheel', () => {
|
|
352
|
+
it('ignores non-ctrl/meta scroll', () => {
|
|
353
|
+
const editor = createEditorWithMockPlumber();
|
|
354
|
+
(editor as any).zoom = 0.5;
|
|
355
|
+
|
|
356
|
+
const event = new WheelEvent('wheel', {
|
|
357
|
+
deltaY: 100,
|
|
358
|
+
clientX: 400,
|
|
359
|
+
clientY: 300
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
(editor as any).handleWheel(event);
|
|
363
|
+
expect((editor as any).zoom).to.equal(0.5);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('zooms out by 0.05 on Ctrl+scroll-down', () => {
|
|
367
|
+
const editor = createEditorWithMockPlumber();
|
|
368
|
+
(editor as any).zoom = 0.5;
|
|
369
|
+
|
|
370
|
+
// Stub querySelector for setZoom's editor lookup
|
|
371
|
+
stub(editor, 'querySelector').returns(null);
|
|
372
|
+
|
|
373
|
+
const event = new WheelEvent('wheel', {
|
|
374
|
+
ctrlKey: true,
|
|
375
|
+
deltaY: 100,
|
|
376
|
+
clientX: 400,
|
|
377
|
+
clientY: 300
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
(editor as any).handleWheel(event);
|
|
381
|
+
expect((editor as any).zoom).to.equal(0.45);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('zooms in by 0.05 on Meta+scroll-up', () => {
|
|
385
|
+
const editor = createEditorWithMockPlumber();
|
|
386
|
+
(editor as any).zoom = 0.5;
|
|
387
|
+
|
|
388
|
+
stub(editor, 'querySelector').returns(null);
|
|
389
|
+
|
|
390
|
+
const event = new WheelEvent('wheel', {
|
|
391
|
+
metaKey: true,
|
|
392
|
+
deltaY: -100,
|
|
393
|
+
clientX: 400,
|
|
394
|
+
clientY: 300
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
(editor as any).handleWheel(event);
|
|
398
|
+
expect((editor as any).zoom).to.equal(0.55);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('calls preventDefault on ctrl/meta wheel events', () => {
|
|
402
|
+
const editor = createEditorWithMockPlumber();
|
|
403
|
+
(editor as any).zoom = 0.5;
|
|
404
|
+
|
|
405
|
+
stub(editor, 'querySelector').returns(null);
|
|
406
|
+
|
|
407
|
+
const event = new WheelEvent('wheel', {
|
|
408
|
+
ctrlKey: true,
|
|
409
|
+
deltaY: 100,
|
|
410
|
+
clientX: 400,
|
|
411
|
+
clientY: 300,
|
|
412
|
+
cancelable: true
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const preventDefaultSpy = spy(event, 'preventDefault');
|
|
416
|
+
(editor as any).handleWheel(event);
|
|
417
|
+
expect(preventDefaultSpy).to.have.been.calledOnce;
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// --- E. Coordinate conversions ---
|
|
422
|
+
|
|
423
|
+
describe('coordinate conversions', () => {
|
|
424
|
+
it('handleCanvasContextMenu divides by zoom', () => {
|
|
425
|
+
const editor = createEditorWithMockPlumber();
|
|
426
|
+
(editor as any).zoom = 0.5;
|
|
427
|
+
(editor as any).viewingRevision = null;
|
|
428
|
+
(editor as any).isTranslating = false;
|
|
429
|
+
(editor as any).definition = { nodes: [{ uuid: 'n' }] };
|
|
430
|
+
|
|
431
|
+
// Track the canvas-space position passed to canvasMenu.show
|
|
432
|
+
// show() is called with: (clientX, clientY, {x: snappedLeft, y: snappedTop}, true, hasNodes)
|
|
433
|
+
let capturedPosition: any = null;
|
|
434
|
+
const mockCanvasMenu = {
|
|
435
|
+
show: (
|
|
436
|
+
_clientX: number,
|
|
437
|
+
_clientY: number,
|
|
438
|
+
position: any,
|
|
439
|
+
..._rest: any[]
|
|
440
|
+
) => {
|
|
441
|
+
capturedPosition = position;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const mockCanvas = {
|
|
446
|
+
getBoundingClientRect: () => ({
|
|
447
|
+
left: 0,
|
|
448
|
+
top: 0,
|
|
449
|
+
width: 1000,
|
|
450
|
+
height: 800
|
|
451
|
+
})
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
stub(editor, 'querySelector').callsFake(((selector: string) => {
|
|
455
|
+
if (selector === '#canvas') return mockCanvas;
|
|
456
|
+
if (selector === 'temba-canvas-menu') return mockCanvasMenu;
|
|
457
|
+
return null;
|
|
458
|
+
}) as any);
|
|
459
|
+
|
|
460
|
+
const event = {
|
|
461
|
+
clientX: 200,
|
|
462
|
+
clientY: 300,
|
|
463
|
+
target: { id: 'canvas' },
|
|
464
|
+
preventDefault: stub(),
|
|
465
|
+
stopPropagation: stub()
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
(editor as any).handleCanvasContextMenu(event);
|
|
469
|
+
|
|
470
|
+
// relativeX = (200 - 0) / 0.5 - 10 = 390, snapped to grid(20): 400
|
|
471
|
+
// relativeY = (300 - 0) / 0.5 - 10 = 590, snapped to grid(20): 600
|
|
472
|
+
expect(capturedPosition).to.not.be.null;
|
|
473
|
+
expect(capturedPosition.x).to.equal(400);
|
|
474
|
+
expect(capturedPosition.y).to.equal(600);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('calculateCanvasDropPosition divides by zoom', () => {
|
|
478
|
+
const editor = createEditorWithMockPlumber();
|
|
479
|
+
(editor as any).zoom = 0.5;
|
|
480
|
+
|
|
481
|
+
const mockCanvas = {
|
|
482
|
+
getBoundingClientRect: () => ({
|
|
483
|
+
left: 0,
|
|
484
|
+
top: 0,
|
|
485
|
+
width: 1000,
|
|
486
|
+
height: 800
|
|
487
|
+
})
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
stub(editor, 'querySelector').callsFake(((selector: string) => {
|
|
491
|
+
if (selector === '#canvas') return mockCanvas;
|
|
492
|
+
return null;
|
|
493
|
+
}) as any);
|
|
494
|
+
|
|
495
|
+
// DROP_PREVIEW_OFFSET_X = 20, DROP_PREVIEW_OFFSET_Y = 20
|
|
496
|
+
const pos = (editor as any).calculateCanvasDropPosition(200, 300, true);
|
|
497
|
+
|
|
498
|
+
// left = (200 - 0) / 0.5 - 20 = 380, snapped: 380
|
|
499
|
+
// top = (300 - 0) / 0.5 - 20 = 580, snapped: 580
|
|
500
|
+
expect(pos.left).to.equal(380);
|
|
501
|
+
expect(pos.top).to.equal(580);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('focusNode multiplies by zoom for scroll position', () => {
|
|
505
|
+
const editor = createEditorWithMockPlumber();
|
|
506
|
+
(editor as any).zoom = 0.5;
|
|
507
|
+
|
|
508
|
+
const mockNode = {
|
|
509
|
+
offsetLeft: 200,
|
|
510
|
+
offsetTop: 300,
|
|
511
|
+
offsetWidth: 100,
|
|
512
|
+
offsetHeight: 60
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
let scrollToArgs: any = null;
|
|
516
|
+
const mockEditor = {
|
|
517
|
+
getBoundingClientRect: () => ({
|
|
518
|
+
width: 800,
|
|
519
|
+
height: 600,
|
|
520
|
+
left: 0,
|
|
521
|
+
top: 0
|
|
522
|
+
}),
|
|
523
|
+
scrollTo: (args: any) => {
|
|
524
|
+
scrollToArgs = args;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
stub(editor, 'querySelector').callsFake(((selector: string) => {
|
|
529
|
+
if (selector.includes('temba-flow-node')) return mockNode;
|
|
530
|
+
if (selector === '#editor') return mockEditor;
|
|
531
|
+
return null;
|
|
532
|
+
}) as any);
|
|
533
|
+
|
|
534
|
+
editor.focusNode('test-uuid');
|
|
535
|
+
|
|
536
|
+
// nodeCenterX = 200 + 50 = 250, nodeCenterY = 300 + 30 = 330
|
|
537
|
+
// targetScrollX = 250 * 0.5 - 400 = -275 -> max(0, -275) = 0
|
|
538
|
+
// targetScrollY = 330 * 0.5 - 300 = -135 -> max(0, -135) = 0
|
|
539
|
+
expect(scrollToArgs).to.not.be.null;
|
|
540
|
+
expect(scrollToArgs.left).to.equal(0);
|
|
541
|
+
expect(scrollToArgs.top).to.equal(0);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('focusNode scroll positions scale with zoom for distant nodes', () => {
|
|
545
|
+
const editor = createEditorWithMockPlumber();
|
|
546
|
+
(editor as any).zoom = 0.5;
|
|
547
|
+
|
|
548
|
+
const mockNode = {
|
|
549
|
+
offsetLeft: 2000,
|
|
550
|
+
offsetTop: 1500,
|
|
551
|
+
offsetWidth: 200,
|
|
552
|
+
offsetHeight: 100
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
let scrollToArgs: any = null;
|
|
556
|
+
const mockEditor = {
|
|
557
|
+
getBoundingClientRect: () => ({
|
|
558
|
+
width: 800,
|
|
559
|
+
height: 600,
|
|
560
|
+
left: 0,
|
|
561
|
+
top: 0
|
|
562
|
+
}),
|
|
563
|
+
scrollTo: (args: any) => {
|
|
564
|
+
scrollToArgs = args;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
stub(editor, 'querySelector').callsFake(((selector: string) => {
|
|
569
|
+
if (selector.includes('temba-flow-node')) return mockNode;
|
|
570
|
+
if (selector === '#editor') return mockEditor;
|
|
571
|
+
return null;
|
|
572
|
+
}) as any);
|
|
573
|
+
|
|
574
|
+
editor.focusNode('distant-node');
|
|
575
|
+
|
|
576
|
+
// nodeCenterX = 2000 + 100 = 2100, nodeCenterY = 1500 + 50 = 1550
|
|
577
|
+
// targetScrollX = 2100 * 0.5 - 400 = 650
|
|
578
|
+
// targetScrollY = 1550 * 0.5 - 300 = 475
|
|
579
|
+
expect(scrollToArgs.left).to.equal(650);
|
|
580
|
+
expect(scrollToArgs.top).to.equal(475);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
});
|