@dxos/app-graph 0.4.7-main.d28a540 → 0.4.7-main.e015b9e

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/src/graph.test.ts CHANGED
@@ -2,315 +2,219 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { effect } from '@preact/signals-core';
6
5
  import { expect } from 'chai';
7
6
 
8
7
  import { describe, test } from '@dxos/test';
9
8
 
10
- import { Graph } from './graph';
11
- import { type Node, type NodeFilter } from './node';
9
+ import { GraphBuilder } from './graph-builder';
10
+ import { createTestNodeBuilder } from './testing';
12
11
 
13
- const longestPaths = new Map<string, string[]>();
14
-
15
- const filterLongestPath: NodeFilter = (node, connectedNode): node is Node => {
16
- const longestPath = longestPaths.get(node.id);
17
- if (!longestPath) {
18
- return false;
19
- }
20
-
21
- if (longestPath[longestPath.length - 2] !== connectedNode.id) {
22
- return false;
23
- }
24
-
25
- return true;
26
- };
27
-
28
- // TODO(wittjosiah): Add tests for granularity of reactivity.
29
12
  describe('Graph', () => {
30
- test('add nodes', () => {
31
- const graph = new Graph();
13
+ test('returns root node', () => {
14
+ const graph = new GraphBuilder().build();
15
+ expect(graph.root).to.not.be.undefined;
16
+ expect(graph.root.id).to.equal('root');
17
+ });
18
+
19
+ test('root node unmodified without builders', () => {
20
+ const graph = new GraphBuilder().build();
21
+ expect(graph.root.properties).to.be.empty;
22
+ expect(graph.root.children).to.be.empty;
23
+ expect(graph.root.actions).to.be.empty;
24
+ });
32
25
 
33
- const [root] = graph.addNodes({
34
- id: 'root',
35
- nodes: [{ id: 'test1' }, { id: 'test2' }],
26
+ test('builder can add children to root node', () => {
27
+ const builder = new GraphBuilder();
28
+ const testNode = { id: 'test', label: 'Test', data: null };
29
+ const id = 'test';
30
+ builder.addNodeBuilder(id, (parent) => {
31
+ if (parent.id === 'root') {
32
+ parent.addNode(id, testNode);
33
+ }
36
34
  });
37
35
 
38
- expect(root.id).to.equal('root');
39
- expect(root.nodes()).to.have.length(2);
40
- expect(graph.findNode('test1')?.id).to.equal('test1');
41
- expect(graph.findNode('test2')?.id).to.equal('test2');
42
- expect(graph.findNode('test1')?.nodes()).to.be.empty;
43
- expect(graph.findNode('test2')?.nodes()).to.be.empty;
44
- expect(graph.findNode('test1')?.nodes({ direction: 'inbound' })).to.have.length(1);
45
- expect(graph.findNode('test2')?.nodes({ direction: 'inbound' })).to.have.length(1);
36
+ const graph = builder.build();
37
+ expect(graph.root.childrenMap.test.id).to.equal(testNode.id);
38
+ expect(graph.root.childrenMap.test.parent!.id).to.equal(graph.root.id);
46
39
  });
47
40
 
48
- test('add nodes updates existing nodes', () => {
49
- const graph = new Graph();
50
-
51
- graph.addNodes({
52
- id: 'root',
53
- nodes: [{ id: 'test1' }, { id: 'test2' }],
54
- });
55
- graph.addNodes({
56
- id: 'root',
57
- nodes: [{ id: 'test1' }, { id: 'test2' }],
41
+ test('builder can add actions to root node', () => {
42
+ const builder = new GraphBuilder();
43
+ const testAction = { id: 'test', label: 'Test', invoke: () => {} };
44
+ builder.addNodeBuilder('test', (parent) => {
45
+ if (parent.id === 'root') {
46
+ parent.addAction(testAction);
47
+ }
58
48
  });
59
49
 
60
- expect(Object.keys(graph._nodes)).to.have.length(3);
61
- expect(Object.keys(graph._edges)).to.have.length(3);
62
- expect(graph.root.nodes()).to.have.length(2);
50
+ const graph = builder.build();
51
+ expect(graph.root.actionsMap.test.id).to.equal(testAction.id);
63
52
  });
64
53
 
65
- test('remove node', () => {
66
- const graph = new Graph();
54
+ test('multiple builders can add children to root node', () => {
55
+ const builder = new GraphBuilder();
56
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1').nodeBuilder);
57
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
67
58
 
68
- const [root] = graph.addNodes({
69
- id: 'root',
70
- nodes: [{ id: 'test1' }, { id: 'test2' }],
71
- });
59
+ const graph = builder.build();
60
+ expect(Object.keys(graph.root.childrenMap)).to.deep.equal(['root-test1', 'root-test2']);
61
+ for (const node of graph.root.children) {
62
+ expect(graph.root.id).to.equal(node.parent!.id);
63
+ }
64
+ });
72
65
 
73
- expect(root.id).to.equal('root');
74
- expect(root.nodes()).to.have.length(2);
75
- expect(graph.findNode('test1')?.id).to.equal('test1');
76
- expect(graph.findNode('test2')?.id).to.equal('test2');
66
+ test('multiple builders can add actions to root node', () => {
67
+ const builder = new GraphBuilder();
68
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1').nodeBuilder);
69
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
77
70
 
78
- graph.removeNode('test1');
79
- expect(graph.findNode('test1')).to.be.undefined;
80
- expect(root.nodes()).to.have.length(1);
71
+ const graph = builder.build();
72
+ expect(Object.keys(graph.root.actionsMap)).to.deep.equal(['root-test1', 'root-test2']);
81
73
  });
82
74
 
83
- test('add edge', () => {
84
- const graph = new Graph();
75
+ test('builders can add children to child node', () => {
76
+ const builder = new GraphBuilder();
77
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1', 2).nodeBuilder);
78
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
85
79
 
86
- graph.addNodes({
87
- id: 'root',
88
- nodes: [{ id: 'test1' }, { id: 'test2' }],
89
- });
90
- graph.addEdge({ source: 'test1', target: 'test2' });
91
-
92
- expect(graph.findNode('test1')?.nodes()).to.have.length(1);
93
- expect(graph.findNode('test2')?.nodes({ direction: 'inbound' })).to.have.length(2);
80
+ const graph = builder.build();
81
+ expect(Object.keys(graph.root.childrenMap['root-test1'].children)).to.be.empty;
82
+ expect(Object.keys(graph.root.childrenMap['root-test2'].childrenMap)).to.deep.equal(['root-test2-test1']);
83
+ for (const node of graph.root.childrenMap['root-test2'].children) {
84
+ expect(graph.root.childrenMap['root-test2'].id).to.equal(node.parent!.id);
85
+ }
94
86
  });
95
87
 
96
- test('add edges is idempontent', () => {
97
- const graph = new Graph();
88
+ test('builders can add actions to child node', () => {
89
+ const builder = new GraphBuilder();
90
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1', 2).nodeBuilder);
91
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
92
+
93
+ const graph = builder.build();
94
+ expect(Object.keys(graph.root.childrenMap['root-test1'].actionsMap)).to.be.empty;
95
+ expect(Object.keys(graph.root.childrenMap['root-test2'].actionsMap)).to.deep.equal(['root-test2-test1']);
96
+ });
98
97
 
99
- graph.addNodes({
100
- id: 'root',
101
- nodes: [{ id: 'test1' }, { id: 'test2' }],
98
+ test('builders can add actions to action sets', () => {
99
+ const builder = new GraphBuilder();
100
+ builder.addNodeBuilder('test', (parent) => {
101
+ const [set] = parent.addAction({ id: 'test-set', label: 'Test Set', invoke: () => {} });
102
+ set.addAction({ id: 'test-action', label: 'Test', invoke: () => {} });
102
103
  });
103
- graph.addEdge({ source: 'test1', target: 'test2' });
104
- graph.addEdge({ source: 'test1', target: 'test2' });
105
104
 
106
- expect(graph.findNode('test1')?.nodes()).to.have.length(1);
107
- expect(graph.findNode('test2')?.nodes({ direction: 'inbound' })).to.have.length(2);
105
+ const graph = builder.build();
106
+ expect(Object.keys(graph.root.actionsMap['test-set'].actionsMap)).to.deep.equal(['test-action']);
108
107
  });
109
108
 
110
- test('sort edges', () => {
111
- const graph = new Graph();
109
+ test('is updated when nodes change', async () => {
110
+ const builder = new GraphBuilder();
112
111
 
113
- const [root] = graph.addNodes({
114
- id: 'root',
115
- nodes: [{ id: 'test1' }, { id: 'test3' }, { id: 'test2' }, { id: 'test4' }],
116
- });
117
-
118
- expect(root.nodes().map((node) => node.id)).to.deep.equal(['test1', 'test3', 'test2', 'test4']);
112
+ // TODO(burdon): Return TestNodeBuilder object.
113
+ const { nodeBuilder, addNode, removeNode, addAction, removeAction, addProperty, removeProperty } =
114
+ createTestNodeBuilder('test1', 2);
115
+ builder.addNodeBuilder('test1', nodeBuilder);
119
116
 
120
- graph.sortEdges('root', 'outbound', ['test4', 'test3']);
117
+ const graph = builder.build();
118
+ expect(graph.root.children).to.have.length(1);
119
+ addNode('root', { id: 'root-test2', label: 'root-test2' });
120
+ expect(graph.root.children).to.have.length(2);
121
+ expect(graph.root.childrenMap['root-test2'].children).to.have.length(0);
121
122
 
122
- expect(root.nodes().map((node) => node.id)).to.deep.equal(['test4', 'test3', 'test1', 'test2']);
123
- });
123
+ expect(graph.root.childrenMap['root-test1'].children).to.have.length(0);
124
+ addNode('root-test1', { id: 'root-test1-test2', label: 'root-test1-test2' });
125
+ expect(graph.root.childrenMap['root-test1'].children).to.have.length(1);
126
+ expect(graph.root.childrenMap['root-test1'].childrenMap['root-test1-test2'].children).to.have.length(0);
124
127
 
125
- test('remove edge', () => {
126
- const graph = new Graph();
128
+ expect(graph.root.actions).to.have.length(1);
129
+ addAction('root', { id: 'root-test2', label: 'root-test2', invoke: () => {} });
130
+ expect(graph.root.actions).to.have.length(2);
127
131
 
128
- graph.addNodes({
129
- id: 'root',
130
- nodes: [{ id: 'test1' }, { id: 'test2' }],
131
- });
132
- graph.removeEdge({ source: 'root', target: 'test1' });
132
+ expect(graph.root.childrenMap['root-test1'].properties).to.not.have.property('test');
133
+ addProperty('root-test1', 'test', 'test');
134
+ expect(graph.root.childrenMap['root-test1'].properties.test).to.equal('test');
133
135
 
134
- expect(graph.root.nodes()).to.have.length(1);
135
- expect(graph.findNode('test1')?.nodes({ direction: 'inbound' })).to.be.empty;
136
- });
136
+ expect(graph.root.children).to.have.length(2);
137
+ removeNode('root', 'root-test2');
138
+ expect(graph.root.children).to.have.length(1);
137
139
 
138
- test('toJSON', () => {
139
- const graph = new Graph();
140
+ expect(graph.root.childrenMap['root-test1'].children).to.have.length(1);
141
+ removeNode('root-test1', 'root-test1-test2');
142
+ expect(graph.root.childrenMap['root-test1'].children).to.have.length(0);
140
143
 
141
- graph.addNodes({
142
- id: 'root',
143
- nodes: [{ id: 'test1' }, { id: 'test2' }],
144
- });
145
- graph.addEdge({ source: 'test1', target: 'test2' });
144
+ expect(graph.root.actions).to.have.length(2);
145
+ removeAction('root', 'root-test2');
146
+ expect(graph.root.actions).to.have.length(1);
146
147
 
147
- const json = graph.toJSON();
148
- expect(json).to.deep.equal({
149
- id: 'root',
150
- nodes: [{ id: 'test1', nodes: [{ id: 'test2' }] }, { id: 'test2' }],
151
- });
148
+ expect(graph.root.childrenMap['root-test1'].properties.test).to.equal('test');
149
+ removeProperty('root-test1', 'test');
150
+ expect(graph.root.childrenMap['root-test1'].properties).to.not.have.property('test');
152
151
  });
153
152
 
154
- test('can be traversed', () => {
155
- const graph = new Graph();
153
+ test.skip('node builder unsubscribe is called when builder is re-run', async () => {
154
+ // TODO(wittjosiah): Implement.
155
+ });
156
156
 
157
- const [root] = graph.addNodes({
158
- id: 'root',
159
- nodes: [{ id: 'test1' }, { id: 'test2' }],
160
- });
157
+ test('can find nodes', () => {
158
+ const builder = new GraphBuilder();
159
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1', 2).nodeBuilder);
160
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
161
161
 
162
- const nodes: string[] = [];
163
- graph.traverse({ node: root, visitor: (node) => nodes.push(node.id) });
164
- expect(nodes).to.deep.equal(['root', 'test1', 'test2']);
162
+ const graph = builder.build();
163
+ expect(graph.findNode('root-test1')?.id).to.equal('root-test1');
164
+ expect(graph.findNode('root-test2-test1')?.id).to.equal('root-test2-test1');
165
165
  });
166
166
 
167
- test('traversal breaks cycles', () => {
168
- const graph = new Graph();
169
-
170
- const [root] = graph.addNodes({
171
- id: 'root',
172
- nodes: [{ id: 'test1' }, { id: 'test2' }],
173
- });
174
- graph.addEdge({ source: 'test1', target: 'root' });
167
+ test('can be traversed', () => {
168
+ const builder = new GraphBuilder();
169
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1', 2).nodeBuilder);
170
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
175
171
 
172
+ const graph = builder.build();
176
173
  const nodes: string[] = [];
177
- graph.traverse({ node: root, visitor: (node) => nodes.push(node.id) });
178
- expect(nodes).to.deep.equal(['root', 'test1', 'test2']);
174
+ graph.traverse({ visitor: (node) => nodes.push(node.id) });
175
+ expect(nodes).to.deep.equal(['root', 'root-test1', 'root-test2', 'root-test2-test1']);
179
176
  });
180
177
 
181
178
  test('traversal can be limited by predicate', () => {
182
- const graph = new Graph();
183
-
184
- const [root] = graph.addNodes({
185
- id: 'root',
186
- nodes: [{ id: 'test1' }, { id: 'test2' }, { id: 'test3' }, { id: 'test4' }],
187
- });
179
+ const builder = new GraphBuilder();
180
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1', 2).nodeBuilder);
181
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
188
182
 
183
+ const graph = builder.build();
189
184
  const nodes: string[] = [];
190
185
  graph.traverse({
191
- node: root,
186
+ filter: (node) => node.id.includes('test1'),
192
187
  visitor: (node) => nodes.push(node.id),
193
- filter: (node) => {
194
- try {
195
- const id = parseInt(node.id.replace('test', ''), 10);
196
- return id % 2 === 0;
197
- } catch (e) {
198
- return false;
199
- }
200
- },
201
188
  });
202
- expect(nodes).to.deep.equal(['test2', 'test4']);
189
+ expect(nodes).to.deep.equal(['root-test1', 'root-test2-test1']);
203
190
  });
204
191
 
205
192
  test('traversal can be started from any node', () => {
206
- const graph = new Graph();
207
-
208
- graph.addNodes({
209
- id: 'root',
210
- nodes: [{ id: 'test1', nodes: [{ id: 'test2', nodes: [{ id: 'test3' }] }] }],
211
- });
193
+ const builder = new GraphBuilder();
194
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1', 2).nodeBuilder);
195
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
212
196
 
197
+ const graph = builder.build();
213
198
  const nodes: string[] = [];
214
199
  graph.traverse({
215
- node: graph.findNode('test2')!,
200
+ node: graph.root.childrenMap['root-test2'],
216
201
  visitor: (node) => nodes.push(node.id),
217
202
  });
218
- expect(nodes).to.deep.equal(['test2', 'test3']);
203
+ expect(nodes).to.deep.equal(['root-test2', 'root-test2-test1']);
219
204
  });
220
205
 
221
- test('traversal can follow inbound edges', () => {
222
- const graph = new Graph();
223
-
224
- graph.addNodes({
225
- id: 'root',
226
- nodes: [{ id: 'test1', nodes: [{ id: 'test2', nodes: [{ id: 'test3' }] }] }],
227
- });
206
+ test('can traverse up', () => {
207
+ const builder = new GraphBuilder();
208
+ builder.addNodeBuilder('test1', createTestNodeBuilder('test1', 2).nodeBuilder);
209
+ builder.addNodeBuilder('test2', createTestNodeBuilder('test2').nodeBuilder);
228
210
 
211
+ const graph = builder.build();
229
212
  const nodes: string[] = [];
230
213
  graph.traverse({
231
- node: graph.findNode('test2')!,
232
- direction: 'inbound',
214
+ direction: 'up',
215
+ node: graph.root.childrenMap['root-test2'].childrenMap['root-test2-test1'],
233
216
  visitor: (node) => nodes.push(node.id),
234
217
  });
235
- expect(nodes).to.deep.equal(['test2', 'test1', 'root']);
236
- });
237
-
238
- test('can filter to longest pathes', () => {
239
- const graph = new Graph();
240
-
241
- graph.addNodes({
242
- id: 'root',
243
- nodes: [{ id: 'test1' }, { id: 'test2' }],
244
- });
245
- graph.addEdge({ source: 'test1', target: 'test2' });
246
-
247
- graph.traverse({
248
- visitor: (node, path) => {
249
- if (!longestPaths.has(node.id) || longestPaths.get(node.id)!.length < path.length) {
250
- longestPaths.set(node.id, path);
251
- }
252
- },
253
- });
254
-
255
- expect(longestPaths.get('root')).to.deep.equal(['root']);
256
- expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
257
- expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
258
- expect(graph.root.nodes({ filter: filterLongestPath })).to.have.length(1);
259
- expect(graph.findNode('test1')?.nodes({ filter: filterLongestPath })).to.have.length(1);
260
- expect(graph.findNode('test2')?.nodes({ filter: filterLongestPath })).to.be.empty;
261
-
262
- longestPaths.clear();
263
- });
264
-
265
- test('traversing the graph subscribes to changes', () => {
266
- const graph = new Graph();
267
-
268
- graph.addNodes({
269
- id: 'root',
270
- nodes: [{ id: 'test1' }, { id: 'test2' }],
271
- });
272
-
273
- const dispose = effect(() => {
274
- graph.traverse({
275
- visitor: (node, path) => {
276
- if (!longestPaths.has(node.id) || longestPaths.get(node.id)!.length < path.length) {
277
- longestPaths.set(node.id, path);
278
- }
279
- },
280
- });
281
- });
282
-
283
- expect(longestPaths.get('root')).to.deep.equal(['root']);
284
- expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
285
- expect(longestPaths.get('test2')).to.deep.equal(['root', 'test2']);
286
- expect(graph.root.nodes({ filter: filterLongestPath })).to.have.length(2);
287
- expect(graph.findNode('test1')?.nodes({ filter: filterLongestPath })).to.be.empty;
288
- expect(graph.findNode('test2')?.nodes({ filter: filterLongestPath })).to.be.empty;
289
-
290
- graph.addEdge({ source: 'test1', target: 'test2' });
291
-
292
- expect(longestPaths.get('root')).to.deep.equal(['root']);
293
- expect(longestPaths.get('test1')).to.deep.equal(['root', 'test1']);
294
- expect(longestPaths.get('test2')).to.deep.equal(['root', 'test1', 'test2']);
295
- expect(graph.root.nodes({ filter: filterLongestPath })).to.have.length(1);
296
- expect(graph.findNode('test1')?.nodes({ filter: filterLongestPath })).to.have.length(1);
297
- expect(graph.findNode('test2')?.nodes({ filter: filterLongestPath })).to.be.empty;
298
-
299
- dispose();
300
- longestPaths.clear();
301
- });
302
-
303
- test('get path', () => {
304
- const graph = new Graph();
305
-
306
- graph.addNodes({
307
- id: 'root',
308
- nodes: [{ id: 'test1' }, { id: 'test2' }],
309
- });
310
- graph.addEdge({ source: 'test1', target: 'test2' });
311
-
312
- expect(graph.getPath({ target: 'test2' })).to.deep.equal(['root', 'test1', 'test2']);
313
- expect(graph.getPath({ source: 'test1', target: 'test2' })).to.deep.equal(['test1', 'test2']);
314
- expect(graph.getPath({ source: 'test2', target: 'test1' })).to.be.undefined;
218
+ expect(nodes).to.deep.equal(['root-test2-test1', 'root-test2', 'root']);
315
219
  });
316
220
  });