@nyaruka/temba-components 0.141.0 → 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 +22 -0
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +859 -656
- 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 +665 -67
- 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/src/live/ContactChat.js +10 -1
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/version.js +9 -0
- package/out-tsc/src/version.js.map +1 -0
- 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-contact-chat.test.js +12 -0
- package/out-tsc/test/temba-contact-chat.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 +164 -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/rollup.components.mjs +7 -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 +769 -76
- 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/src/live/ContactChat.ts +10 -1
- package/src/store/flow-definition.d.ts +1 -0
- package/src/version.ts +10 -0
- 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-contact-chat.test.ts +17 -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 +211 -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/web-dev-server.config.mjs +5 -1
- package/web-test-runner.config.mjs +4 -1
- 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,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
|
+
});
|