@elicdavis/node-flow 0.2.3 → 0.2.4

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.
@@ -1,7 +1,7 @@
1
1
  import { strict as assert } from 'assert';
2
2
  import { BuildMarkdown } from '../src/markdown/markdown';
3
3
  import { CodeBlockEntry } from '../src/markdown/entry';
4
- import { Theme } from '../src/theme';
4
+ import { Theme, setTheme } from '../src/theme';
5
5
 
6
6
  interface Drawn {
7
7
  box: { y: number; height: number } | null;
@@ -49,6 +49,26 @@ describe('a markdown code block', () => {
49
49
  assert.deepEqual(drawn.lines.map((l) => l.text), ['const a = 1;', 'const b = 2;']);
50
50
  });
51
51
 
52
+ it('draws in the theme code font, apart from the note text', () => {
53
+ setTheme({ FontFamily: 'Tahoma', Note: { CodeFontFamily: 'VT323' } });
54
+ try {
55
+ const entry = BuildMarkdown('```\none\n```')[0] as CodeBlockEntry;
56
+ assert.equal(entry.text().style().getFont(), 'VT323');
57
+ } finally {
58
+ setTheme();
59
+ }
60
+ });
61
+
62
+ it('draws in the theme font when no code font is set', () => {
63
+ setTheme({ FontFamily: 'Tahoma' });
64
+ try {
65
+ const entry = BuildMarkdown('```\none\n```')[0] as CodeBlockEntry;
66
+ assert.equal(entry.text().style().getFont(), 'Tahoma');
67
+ } finally {
68
+ setTheme();
69
+ }
70
+ });
71
+
52
72
  it('reports the height it drew', () => {
53
73
  const entry = BuildMarkdown('```\none\n```')[0] as CodeBlockEntry;
54
74
  const drawn: Drawn = { box: null, lines: [] };
@@ -0,0 +1,70 @@
1
+ import { strict as assert } from 'assert';
2
+ import { MouseObserver } from '../src/input';
3
+
4
+ function observe(run: (fire: (name: string, event?: any) => void, fireDocument: (name: string, event?: any) => void, left: () => number) => void) {
5
+ const listeners = new Map<string, Array<(e: any) => void>>();
6
+ const documentListeners = new Map<string, Array<(e: any) => void>>();
7
+ const ele: any = {
8
+ style: {},
9
+ addEventListener: (name: string, fn: (e: any) => void) => {
10
+ listeners.set(name, [...(listeners.get(name) ?? []), fn]);
11
+ },
12
+ getBoundingClientRect: () => ({ left: 0, top: 0 }),
13
+ };
14
+ const dispatch = (from: Map<string, Array<(e: any) => void>>) => (name: string, event: any = {}) => {
15
+ event.preventDefault = () => { };
16
+ event.button ??= 0;
17
+ event.clientX ??= 10;
18
+ event.clientY ??= 10;
19
+ (from.get(name) ?? []).forEach((fn) => fn(event));
20
+ };
21
+
22
+ let leaves = 0;
23
+ const g = globalThis as any;
24
+ const previous = g.document;
25
+ g.document = {
26
+ addEventListener: (name: string, fn: (e: any) => void) => {
27
+ documentListeners.set(name, [...(documentListeners.get(name) ?? []), fn]);
28
+ },
29
+ };
30
+ try {
31
+ new MouseObserver(ele, () => { }, () => { }, () => { }, () => { }, () => { }, () => { }, () => { },
32
+ () => { leaves++; });
33
+ run(dispatch(listeners), dispatch(documentListeners), () => leaves);
34
+ } finally {
35
+ g.document = previous;
36
+ }
37
+ }
38
+
39
+ describe('the mouse leaving the canvas', () => {
40
+ it('is reported when nothing is held', () => {
41
+ observe((fire, _, left) => {
42
+ fire('mousemove');
43
+ fire('mouseleave');
44
+ assert.equal(left(), 1);
45
+ });
46
+ });
47
+
48
+ it('is reported once a drag that left the canvas is let go', () => {
49
+ observe((fire, fireDocument, left) => {
50
+ fire('mousemove');
51
+ fire('mousedown');
52
+ fire('mouseleave');
53
+ assert.equal(left(), 0, 'still dragging');
54
+
55
+ fireDocument('mouseup');
56
+ assert.equal(left(), 1);
57
+ });
58
+ });
59
+
60
+ it('is not reported when a drag comes back before it is let go', () => {
61
+ observe((fire, fireDocument, left) => {
62
+ fire('mousemove');
63
+ fire('mousedown');
64
+ fire('mouseleave');
65
+ fire('mousemove');
66
+ fireDocument('mouseup');
67
+ assert.equal(left(), 0);
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,69 @@
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 { Theme } from '../src/theme';
7
+ import { center, stubCtx } from './support';
8
+
9
+ describe('the node hover listener', () => {
10
+ it('reports the node under the mouse once, then null when the mouse leaves', () => {
11
+ const graph = new NodeSubsystem(new PassSubsystem());
12
+ const node = new FlowNode({ title: 'hovered', position: { x: 100, y: 100 } });
13
+ graph.addNode(node);
14
+ const ctx = stubCtx();
15
+ const camera = new Camera();
16
+ const seen: Array<FlowNode | null> = [];
17
+ graph.addNodeHoverListener((n) => seen.push(n));
18
+
19
+ graph.render(ctx, camera, undefined);
20
+ const over = center(node.calculateBounds(ctx, camera));
21
+ graph.render(ctx, camera, over);
22
+ graph.render(ctx, camera, over);
23
+ graph.render(ctx, camera, undefined);
24
+
25
+ assert.deepEqual(seen, [node, null]);
26
+ });
27
+ });
28
+
29
+ describe('a highlighted node', () => {
30
+ it('keeps a screen-width outline in the highlight colour when zoomed far out', () => {
31
+ const node = new FlowNode({ title: 'lit', position: { x: 100, y: 100 } });
32
+ const graph = new NodeSubsystem(new PassSubsystem());
33
+ graph.addNode(node);
34
+ const strokes: Array<{ style: string; width: number }> = [];
35
+ const ctx = stubCtx();
36
+ (ctx as any).stroke = function () { strokes.push({ style: this.strokeStyle, width: this.lineWidth }); };
37
+ const camera = new Camera();
38
+ camera.zoom = 0.1;
39
+
40
+ node.setHighlighted(true);
41
+ graph.render(ctx, camera, undefined);
42
+
43
+ assert.ok(strokes.some((s) => s.style === Theme.Node.Border.Highlighted && s.width === 2));
44
+ });
45
+
46
+ it('draws its ports with the same line width as when it is not highlighted', () => {
47
+ const widths = (highlighted: boolean) => {
48
+ const node = new FlowNode({ title: 'lit', position: { x: 100, y: 100 }, inputs: [{ name: 'In', type: 'float64' }] });
49
+ const graph = new NodeSubsystem(new PassSubsystem());
50
+ graph.addNode(node);
51
+ const strokes: Array<{ style: string; width: number }> = [];
52
+ const ctx = stubCtx();
53
+ let width = 1;
54
+ Object.defineProperty(ctx, 'lineWidth', { get: () => width, set: (w: number) => { width = w; } });
55
+ const saved: Array<number> = [];
56
+ (ctx as any).save = () => saved.push(width);
57
+ (ctx as any).restore = () => { width = saved.pop() ?? width; };
58
+ (ctx as any).stroke = function () { strokes.push({ style: this.strokeStyle, width }); };
59
+ const camera = new Camera();
60
+ camera.zoom = 0.1;
61
+
62
+ node.setHighlighted(highlighted);
63
+ graph.render(ctx, camera, undefined);
64
+ return strokes.filter((s) => s.style !== Theme.Node.Border.Highlighted).map((s) => s.width);
65
+ };
66
+
67
+ assert.deepEqual(widths(true), widths(false));
68
+ });
69
+ });
@@ -117,3 +117,27 @@ describe('filtering the new node menu to what could finish a wire', () => {
117
117
  assert.ok(!groups.includes('Math'), `Math should be gone, got ${groups}`);
118
118
  });
119
119
  });
120
+
121
+ describe('filtering the new node menu through casts', () => {
122
+ const graph = {} as any;
123
+ const at = { x: 0, y: 0 };
124
+ const casts = [{ from: 'int', to: 'float64' }];
125
+
126
+ it('dragging an int output offers nodes taking a float64', () => {
127
+ const names = itemNames(publisher().contextMenu(graph, at, {
128
+ needs: PortType.Input,
129
+ types: ['int'],
130
+ casts,
131
+ }));
132
+ assert.deepStrictEqual(names, ['Add', 'Negate', 'Select']);
133
+ });
134
+
135
+ it('dragging an int input does not offer float64 producers', () => {
136
+ const names = itemNames(publisher().contextMenu(graph, at, {
137
+ needs: PortType.Output,
138
+ types: ['int'],
139
+ casts,
140
+ }));
141
+ assert.deepStrictEqual(names, ['Select']);
142
+ });
143
+ });
package/test/port.test.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import * as assert from 'assert';
2
- import { Port, PortConfig, PortType, portsCompatible } from '../src/port';
2
+ import { Port, PortConfig, PortType, portsCompatible, portsConnect } from '../src/port';
3
3
  import { FlowNode } from '../src/node';
4
+ import { DanglingConnection, NodeSubsystem } from '../src/nodes/subsystem';
5
+ import { PassSubsystem } from '../src/pass/subsystem';
6
+ import { Camera } from '../src/camera';
7
+ import { center, stubCtx } from './support';
4
8
 
5
9
  function port(config: PortConfig): Port {
6
10
  // A port only reaches back to its node when it is drawn or connected,
@@ -123,3 +127,47 @@ describe('settling a port on a type', () => {
123
127
  assert.ok(portsCompatible(settled, port({ type: 'float64' })));
124
128
  });
125
129
  });
130
+
131
+ describe('casts', () => {
132
+ const casts = [{ from: 'int', to: 'float64' }];
133
+
134
+ it('lets an output feed an input it casts to', () => {
135
+ assert.ok(portsConnect(port({ type: 'int' }), port({ type: 'float64' }), casts));
136
+ });
137
+
138
+ it('only goes the way it was registered', () => {
139
+ assert.ok(!portsConnect(port({ type: 'float64' }), port({ type: 'int' }), casts));
140
+ });
141
+
142
+ it('reaches any of the types an input takes', () => {
143
+ const lifted = port({ type: 'float64', acceptedTypes: ['float64', '[]float64'] });
144
+ assert.ok(portsConnect(port({ type: 'int' }), lifted, casts));
145
+ });
146
+
147
+ it('refuses mismatched types without one', () => {
148
+ assert.ok(!portsConnect(port({ type: 'int' }), port({ type: 'float64' })));
149
+ });
150
+
151
+ it('wires a node made from a released wire through a cast', () => {
152
+ const graph = new NodeSubsystem(new PassSubsystem(), { casts });
153
+ const source = new FlowNode({ title: 'source', position: { x: 0, y: 0 }, outputs: [{ name: 'Out', type: 'int' }] });
154
+ graph.addNode(source);
155
+ const camera = new Camera();
156
+ const ctx = stubCtx();
157
+ graph.render(ctx, camera, undefined);
158
+
159
+ let dangling: DanglingConnection | null = null;
160
+ graph.setConnectionReleasedHandler((d) => dangling = d);
161
+ const out = center(source.outputPortPosition(0));
162
+ graph.render(ctx, camera, out);
163
+ graph.clickStart(out, camera, false);
164
+ graph.render(ctx, camera, { x: 600, y: 500 });
165
+ graph.clickEnd();
166
+
167
+ const made = new FlowNode({ title: 'made', position: { x: 300, y: 0 }, inputs: [{ name: 'In', type: 'float64' }] });
168
+ graph.addNode(made);
169
+ dangling!.connectTo(made);
170
+
171
+ assert.equal(made.inputPort(0).connections().length, 1);
172
+ });
173
+ });