@elicdavis/node-flow 0.1.20 → 0.2.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/dist/index.d.mts +386 -227
- package/dist/index.d.ts +386 -227
- package/dist/index.js +1866 -1059
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1849 -1058
- package/dist/index.mjs.map +1 -1
- package/index.html +1134 -329
- package/package.json +1 -1
- package/src/connection.ts +30 -5
- package/src/contextMenu.ts +66 -34
- package/src/graph.ts +12 -10
- package/src/index.ts +26 -12
- package/src/markdown/entry.ts +11 -15
- package/src/markdown/syntaxParser.ts +2 -1
- package/src/node.ts +160 -101
- package/src/nodes/subsystem.ts +113 -29
- package/src/notes/note.ts +9 -1
- package/src/organize.ts +540 -111
- package/src/popup.ts +4 -2
- package/src/port.ts +83 -6
- package/src/styles/bevel.ts +31 -0
- package/src/styles/cursor.ts +1 -0
- package/src/styles/text.ts +4 -4
- package/src/theme.ts +179 -92
- package/src/types/text.ts +5 -1
- package/src/utils/color.ts +1 -1
- package/src/widgets/button.ts +52 -107
- package/src/widgets/color.ts +91 -150
- package/src/widgets/factory.ts +89 -46
- package/src/widgets/field.ts +99 -0
- package/src/widgets/image.ts +107 -165
- package/src/widgets/inplace.ts +84 -0
- package/src/widgets/number.ts +122 -154
- package/src/widgets/row.ts +138 -0
- package/src/widgets/slider.ts +101 -216
- package/src/widgets/string.ts +39 -130
- package/src/widgets/text.ts +81 -92
- package/src/widgets/toggle.ts +62 -207
- package/src/widgets/value.ts +127 -0
- package/src/widgets/widget.ts +180 -34
- package/test/context_menu.test.ts +82 -0
- package/test/cursor.test.ts +17 -0
- package/test/hidden_port.test.ts +125 -0
- package/test/markdown.test.ts +2 -2
- package/test/markdown_code_block.test.ts +58 -0
- package/test/multi_input.test.ts +124 -0
- package/test/organize.test.ts +117 -1
- package/test/support.ts +175 -0
- package/test/theme.test.ts +86 -0
- package/test/widget_click.test.ts +111 -97
- package/test/widget_construction.test.ts +5 -4
- package/test/widgets.test.ts +333 -0
- package/test/wire_follows_camera.test.ts +57 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { strict as assert } from 'assert';
|
|
2
|
+
import { FlowNode } from '../src/node';
|
|
3
|
+
import { Box } from '../src/types/box';
|
|
4
|
+
import { Vector2 } from '../src/types/vector2';
|
|
5
|
+
import { NumberWidget } from '../src/widgets/number';
|
|
6
|
+
import { StringWidget } from '../src/widgets/string';
|
|
7
|
+
import { SliderWidget } from '../src/widgets/slider';
|
|
8
|
+
import { TextWidget } from '../src/widgets/text';
|
|
9
|
+
import { ToggleWidget } from '../src/widgets/toggle';
|
|
10
|
+
import { ImageWidget } from '../src/widgets/image';
|
|
11
|
+
import { ColorWidget } from '../src/widgets/color';
|
|
12
|
+
import { RowWidget } from '../src/widgets/row';
|
|
13
|
+
import { WidgetBase } from '../src/widgets/widget';
|
|
14
|
+
import { CanvasRecord, center, stubCtx, widgetScene, withDocument } from './support';
|
|
15
|
+
|
|
16
|
+
describe('a number widget', () => {
|
|
17
|
+
it('follows a drag and reports one edit when let go', () => {
|
|
18
|
+
const inputs: Array<number> = [];
|
|
19
|
+
const commits: Array<number> = [];
|
|
20
|
+
const edits: Array<number> = [];
|
|
21
|
+
const { node, widget, widgetBox, press } = widgetScene((n) => {
|
|
22
|
+
n.addPropertyEditListener('value', (v) => edits.push(v));
|
|
23
|
+
return new NumberWidget(n, {
|
|
24
|
+
property: 'value', value: 10, step: 0.5,
|
|
25
|
+
onInput: (v) => inputs.push(v),
|
|
26
|
+
callback: (v) => commits.push(v),
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
const on = center(widgetBox());
|
|
30
|
+
|
|
31
|
+
const gesture = press(on);
|
|
32
|
+
gesture.moveTo({ x: on.x + 10, y: on.y });
|
|
33
|
+
gesture.moveTo({ x: on.x + 20, y: on.y });
|
|
34
|
+
gesture.release();
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(inputs, [15, 20]);
|
|
37
|
+
assert.deepEqual(commits, [20]);
|
|
38
|
+
assert.deepEqual(edits, [20]);
|
|
39
|
+
assert.equal(node.getProperty('value'), 20);
|
|
40
|
+
assert.equal((widget as NumberWidget).value(), 20);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('keeps on following a drag that leaves the node', () => {
|
|
44
|
+
const { widget, widgetBox, press } = widgetScene((n) => new NumberWidget(n, { value: 0, step: 1 }));
|
|
45
|
+
const on = center(widgetBox());
|
|
46
|
+
|
|
47
|
+
const gesture = press(on);
|
|
48
|
+
gesture.moveTo({ x: on.x + 500, y: on.y + 200 });
|
|
49
|
+
gesture.release();
|
|
50
|
+
|
|
51
|
+
assert.equal((widget as NumberWidget).value(), 500);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rounds an integer while it is dragged', () => {
|
|
55
|
+
const { widget, widgetBox, press } = widgetScene((n) => new NumberWidget(n, { value: 1, integer: true, step: 0.3 }));
|
|
56
|
+
const on = center(widgetBox());
|
|
57
|
+
|
|
58
|
+
const gesture = press(on);
|
|
59
|
+
gesture.moveTo({ x: on.x + 10, y: on.y });
|
|
60
|
+
gesture.release();
|
|
61
|
+
|
|
62
|
+
assert.equal((widget as NumberWidget).value(), 4);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('opens an input over itself when clicked, and keeps what is typed', () => {
|
|
66
|
+
withDocument((body) => {
|
|
67
|
+
const commits: Array<number> = [];
|
|
68
|
+
const { widget, widgetBox, click } = widgetScene((n) => new NumberWidget(n, { value: 3, callback: (v) => commits.push(v) }));
|
|
69
|
+
|
|
70
|
+
click(center(widgetBox()));
|
|
71
|
+
const input = body.children[0];
|
|
72
|
+
assert.ok(input, 'an input was laid over the widget');
|
|
73
|
+
assert.equal(input.value, '3');
|
|
74
|
+
assert.ok(input.focused);
|
|
75
|
+
|
|
76
|
+
input.value = '7.5';
|
|
77
|
+
input.fire('keydown', { key: 'Enter' });
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(commits, [7.5]);
|
|
80
|
+
assert.equal((widget as NumberWidget).value(), 7.5);
|
|
81
|
+
assert.equal(body.children.length, 0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('throws away what was typed on escape', () => {
|
|
86
|
+
withDocument((body) => {
|
|
87
|
+
const commits: Array<number> = [];
|
|
88
|
+
const { widget, widgetBox, click } = widgetScene((n) => new NumberWidget(n, { value: 3, callback: (v) => commits.push(v) }));
|
|
89
|
+
|
|
90
|
+
click(center(widgetBox()));
|
|
91
|
+
const input = body.children[0];
|
|
92
|
+
input.value = '99';
|
|
93
|
+
input.fire('keydown', { key: 'Escape' });
|
|
94
|
+
|
|
95
|
+
assert.deepEqual(commits, []);
|
|
96
|
+
assert.equal((widget as NumberWidget).value(), 3);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not open an input after a drag', () => {
|
|
101
|
+
withDocument((body) => {
|
|
102
|
+
const { widgetBox, press } = widgetScene((n) => new NumberWidget(n, { value: 0 }));
|
|
103
|
+
const on = center(widgetBox());
|
|
104
|
+
const gesture = press(on);
|
|
105
|
+
gesture.moveTo({ x: on.x + 10, y: on.y });
|
|
106
|
+
gesture.release();
|
|
107
|
+
assert.equal(body.children.length, 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('shows a value set from code without reporting it as an edit', () => {
|
|
112
|
+
const commits: Array<number> = [];
|
|
113
|
+
const edits: Array<number> = [];
|
|
114
|
+
const node = new FlowNode({ title: 't' });
|
|
115
|
+
node.addPropertyEditListener('value', (v) => edits.push(v));
|
|
116
|
+
const widget = new NumberWidget(node, { property: 'value', callback: (v) => commits.push(v) });
|
|
117
|
+
|
|
118
|
+
widget.setValue(5);
|
|
119
|
+
node.setProperty('value', 6);
|
|
120
|
+
|
|
121
|
+
assert.equal(widget.value(), 6);
|
|
122
|
+
assert.deepEqual(commits, []);
|
|
123
|
+
assert.deepEqual(edits, []);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('draws its label on the left and its value on the right', () => {
|
|
127
|
+
const record: CanvasRecord = { fills: [], texts: [] };
|
|
128
|
+
const widget = new NumberWidget(new FlowNode({ title: 't' }), { label: 'X', value: 1.5 });
|
|
129
|
+
widget.Draw(stubCtx(record), { Position: { x: 0, y: 0 }, Size: { x: 100, y: 25 } }, 1, undefined);
|
|
130
|
+
|
|
131
|
+
const label = record.texts.find((t) => t.text === 'X');
|
|
132
|
+
const value = record.texts.find((t) => t.text === '1.5');
|
|
133
|
+
assert.ok(label && value);
|
|
134
|
+
assert.ok(label.x < value.x);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('a string widget', () => {
|
|
139
|
+
it('edits in place', () => {
|
|
140
|
+
withDocument((body) => {
|
|
141
|
+
const commits: Array<string> = [];
|
|
142
|
+
const { widgetBox, click } = widgetScene((n) => new StringWidget(n, { value: 'a', callback: (v) => commits.push(v) }));
|
|
143
|
+
|
|
144
|
+
click(center(widgetBox()));
|
|
145
|
+
body.children[0].value = 'b';
|
|
146
|
+
body.children[0].fire('blur');
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(commits, ['b']);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('a slider', () => {
|
|
154
|
+
it('stops following the mouse once released away from it', () => {
|
|
155
|
+
const commits: Array<number> = [];
|
|
156
|
+
const { widget, widgetBox, press, graph, ctx, camera } = widgetScene((n) => new SliderWidget(n, {
|
|
157
|
+
value: 0.5,
|
|
158
|
+
callback: (v) => commits.push(v),
|
|
159
|
+
}));
|
|
160
|
+
const on = center(widgetBox());
|
|
161
|
+
|
|
162
|
+
const gesture = press(on);
|
|
163
|
+
gesture.moveTo({ x: on.x + 400, y: on.y + 300 });
|
|
164
|
+
gesture.release();
|
|
165
|
+
|
|
166
|
+
assert.deepEqual(commits, [1], 'dragging past the end and letting go still finishes the drag');
|
|
167
|
+
graph.render(ctx, camera, { x: 5, y: 500 });
|
|
168
|
+
assert.equal((widget as SliderWidget).value(), 1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('a toggle', () => {
|
|
173
|
+
it('flips when clicked and reports the new value', () => {
|
|
174
|
+
const seen: Array<boolean> = [];
|
|
175
|
+
const { widgetBox, click } = widgetScene((n) => new ToggleWidget(n, { callback: (v) => seen.push(v) }));
|
|
176
|
+
click(center(widgetBox()));
|
|
177
|
+
assert.deepEqual(seen, [true]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('stays put when the press is dragged off before release', () => {
|
|
181
|
+
const seen: Array<boolean> = [];
|
|
182
|
+
const { widgetBox, press } = widgetScene((n) => new ToggleWidget(n, { callback: (v) => seen.push(v) }));
|
|
183
|
+
const on = center(widgetBox());
|
|
184
|
+
const gesture = press(on);
|
|
185
|
+
gesture.moveTo({ x: on.x + 300, y: on.y });
|
|
186
|
+
gesture.release();
|
|
187
|
+
assert.deepEqual(seen, []);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('lights with the colour configured for its off state', () => {
|
|
191
|
+
const record: CanvasRecord = { fills: [], texts: [] };
|
|
192
|
+
const toggle = new ToggleWidget(new FlowNode({ title: 't' }), { onColor: '#abcdef', offColor: '#123456' });
|
|
193
|
+
toggle.Draw(stubCtx(record), { Position: { x: 0, y: 0 }, Size: { x: 150, y: 25 } }, 1, undefined);
|
|
194
|
+
assert.ok(record.fills.includes('#123456'));
|
|
195
|
+
assert.ok(!record.fills.includes('#abcdef'));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('a colour widget', () => {
|
|
200
|
+
it('opens the browser picker on click and keeps its alpha', () => {
|
|
201
|
+
withDocument((body) => {
|
|
202
|
+
const inputs: Array<string> = [];
|
|
203
|
+
const commits: Array<string> = [];
|
|
204
|
+
const { widgetBox, click } = widgetScene((n) => new ColorWidget(n, {
|
|
205
|
+
value: '#ff000080',
|
|
206
|
+
onInput: (c) => inputs.push(c),
|
|
207
|
+
callback: (c) => commits.push(c),
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
click(center(widgetBox()));
|
|
211
|
+
const picker = body.children[0];
|
|
212
|
+
assert.equal(picker.clicked, 1);
|
|
213
|
+
assert.equal(picker.value, '#ff0000');
|
|
214
|
+
|
|
215
|
+
picker.value = '#00ff00';
|
|
216
|
+
picker.oninput?.();
|
|
217
|
+
picker.onchange?.();
|
|
218
|
+
|
|
219
|
+
assert.deepEqual(inputs, ['#00ff0080']);
|
|
220
|
+
assert.deepEqual(commits, ['#00ff0080']);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('opens the picker over the widget just clicked, not the one before', () => {
|
|
225
|
+
withDocument((body) => {
|
|
226
|
+
let first: ColorWidget | undefined;
|
|
227
|
+
const { node, widgetBox, click } = widgetScene((n) => {
|
|
228
|
+
first = new ColorWidget(n, { value: '#ff0000' });
|
|
229
|
+
n.addWidget(first);
|
|
230
|
+
return new ColorWidget(n, { value: '#00ff00' });
|
|
231
|
+
});
|
|
232
|
+
const firstBox = node.widgetPosition(node.widgets().indexOf(first as ColorWidget)) as Box;
|
|
233
|
+
|
|
234
|
+
click(center(firstBox));
|
|
235
|
+
const picker = body.children[0];
|
|
236
|
+
const overFirst = picker.style.cssText;
|
|
237
|
+
click(center(widgetBox()));
|
|
238
|
+
const overSecond = picker.style.cssText;
|
|
239
|
+
|
|
240
|
+
assert.notEqual(overFirst, overSecond);
|
|
241
|
+
assert.deepEqual(picker.openedAt, [overFirst, overSecond]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('writes white on a dark colour that has alpha', () => {
|
|
246
|
+
const record: CanvasRecord = { fills: [], texts: [] };
|
|
247
|
+
const widget = new ColorWidget(new FlowNode({ title: 't' }), { value: '#10101080' });
|
|
248
|
+
widget.Draw(stubCtx(record), { Position: { x: 0, y: 0 }, Size: { x: 150, y: 25 } }, 1, undefined);
|
|
249
|
+
assert.deepEqual(record.texts.map((t) => t.fillStyle), ['white']);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
class Tap extends WidgetBase {
|
|
254
|
+
clicks = 0;
|
|
255
|
+
width: number;
|
|
256
|
+
constructor(width: number) { super(); this.width = width; }
|
|
257
|
+
Size(): Vector2 { return { x: this.width, y: 20 }; }
|
|
258
|
+
protected render(_ctx: CanvasRenderingContext2D, _box: Box): void { }
|
|
259
|
+
protected onClick(): void { this.clicks++; }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
describe('a row', () => {
|
|
263
|
+
it('hands the press to the child under it', () => {
|
|
264
|
+
const left = new Tap(40);
|
|
265
|
+
const right = new Tap(40);
|
|
266
|
+
const { widgetBox, click } = widgetScene(() => new RowWidget([left, right]));
|
|
267
|
+
const box = widgetBox();
|
|
268
|
+
|
|
269
|
+
click({ x: box.Position.x + box.Size.x - 5, y: box.Position.y + 10 });
|
|
270
|
+
|
|
271
|
+
assert.equal(left.clicks, 0);
|
|
272
|
+
assert.equal(right.clicks, 1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('gives the spare room only to children that grow', () => {
|
|
276
|
+
const drawn: Array<Box> = [];
|
|
277
|
+
const spy = (w: number) => new (class extends Tap {
|
|
278
|
+
protected render(_ctx: CanvasRenderingContext2D, box: Box): void { drawn.push(box); }
|
|
279
|
+
})(w);
|
|
280
|
+
const row = new RowWidget([spy(40), spy(20)], { grow: [1, 0], gap: 0 });
|
|
281
|
+
row.Draw(stubCtx(), { Position: { x: 0, y: 0 }, Size: { x: 200, y: 20 } }, 1, undefined);
|
|
282
|
+
|
|
283
|
+
assert.equal(drawn[0].Size.x, 180);
|
|
284
|
+
assert.equal(drawn[1].Size.x, 20);
|
|
285
|
+
assert.equal(drawn[1].Position.x, 180);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('a text widget', () => {
|
|
290
|
+
it('follows the property it is bound to', () => {
|
|
291
|
+
const node = new FlowNode({ title: 't' });
|
|
292
|
+
const text = new TextWidget(node, { property: 'label' });
|
|
293
|
+
node.setProperty('label', 'hi');
|
|
294
|
+
assert.equal(text.text(), 'hi');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
interface StubImage {
|
|
299
|
+
src: string;
|
|
300
|
+
width: number;
|
|
301
|
+
height: number;
|
|
302
|
+
onload?: () => void;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
describe('an image widget', () => {
|
|
306
|
+
it('keeps the newest image when an older one finishes loading last', () => {
|
|
307
|
+
const g = globalThis as any;
|
|
308
|
+
const previous = g.document;
|
|
309
|
+
const images: Array<StubImage> = [];
|
|
310
|
+
g.document = {
|
|
311
|
+
createElement: () => {
|
|
312
|
+
const img: StubImage = { src: '', width: 0, height: 0 };
|
|
313
|
+
images.push(img);
|
|
314
|
+
return img;
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
const widget = new ImageWidget();
|
|
319
|
+
widget.SetUrl('old');
|
|
320
|
+
widget.SetUrl('new');
|
|
321
|
+
const [older, newer] = images;
|
|
322
|
+
newer.width = 100; newer.height = 100;
|
|
323
|
+
older.width = 40; older.height = 40;
|
|
324
|
+
|
|
325
|
+
newer.onload?.();
|
|
326
|
+
older.onload?.();
|
|
327
|
+
|
|
328
|
+
assert.equal(widget.Size().x, 100);
|
|
329
|
+
} finally {
|
|
330
|
+
g.document = previous;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { strict as assert } from 'assert';
|
|
2
|
+
import { NodeSubsystem } from '../src/nodes/subsystem';
|
|
3
|
+
import { PassSubsystem } from '../src/pass/subsystem';
|
|
4
|
+
import { FlowNode } from '../src/node';
|
|
5
|
+
import { Camera } from '../src/camera';
|
|
6
|
+
import { Vector2 } from '../src/types/vector2';
|
|
7
|
+
import { center, stubCtx } from './support';
|
|
8
|
+
|
|
9
|
+
function scene() {
|
|
10
|
+
let drawnTo: Vector2 | null = null;
|
|
11
|
+
const graph = new NodeSubsystem(new PassSubsystem(), {
|
|
12
|
+
idleConnection: {
|
|
13
|
+
renderer: (params) => { drawnTo = { x: params.start.x, y: params.start.y }; },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const from = new FlowNode({ title: 'from', position: { x: 50, y: 50 }, outputs: [{ name: 'out', type: 'n' }] });
|
|
17
|
+
const to = new FlowNode({ title: 'to', position: { x: 300, y: 80 }, inputs: [{ name: 'in', type: 'n' }] });
|
|
18
|
+
graph.addNode(from);
|
|
19
|
+
graph.addNode(to);
|
|
20
|
+
graph.connectNodes(from, 0, to, 0);
|
|
21
|
+
|
|
22
|
+
const camera = new Camera();
|
|
23
|
+
const ctx = stubCtx();
|
|
24
|
+
graph.render(ctx, camera, undefined);
|
|
25
|
+
graph.render(ctx, camera, undefined);
|
|
26
|
+
return { graph, to, camera, ctx, drawnTo: () => drawnTo as Vector2 | null };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('a wire', () => {
|
|
30
|
+
it('meets its port in the first frame after the camera moves', () => {
|
|
31
|
+
const { graph, to, camera, ctx, drawnTo } = scene();
|
|
32
|
+
|
|
33
|
+
camera.position.x += 137;
|
|
34
|
+
camera.position.y -= 41;
|
|
35
|
+
graph.render(ctx, camera, undefined);
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(drawnTo(), center(to.inputPortPosition(0)));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('meets its port in the first frame after the camera zooms', () => {
|
|
41
|
+
const { graph, to, camera, ctx, drawnTo } = scene();
|
|
42
|
+
|
|
43
|
+
camera.zoom = 1.7;
|
|
44
|
+
graph.render(ctx, camera, undefined);
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(drawnTo(), center(to.inputPortPosition(0)));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('meets its port in the first frame after the node is dragged', () => {
|
|
50
|
+
const { graph, to, camera, ctx, drawnTo } = scene();
|
|
51
|
+
|
|
52
|
+
to.translate({ x: 60, y: 25 });
|
|
53
|
+
graph.render(ctx, camera, undefined);
|
|
54
|
+
|
|
55
|
+
assert.deepEqual(drawnTo(), center(to.inputPortPosition(0)));
|
|
56
|
+
});
|
|
57
|
+
});
|