@yuneta/gobj-ui 7.23.65 → 7.23.66

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.
@@ -0,0 +1,308 @@
1
+ /***********************************************************************
2
+ * treedb_fold_model.test.js
3
+ *
4
+ * The fold arithmetic of the treedb graph, pinned.
5
+ *
6
+ * A small treedb in the shape of the real one that forced this:
7
+ * places (a self-referent tree), devices hanging from a place AND
8
+ * a controller, controllers hanging from a place, and a topic with
9
+ * hooks and no fkeys (extended) whose children must be roots.
10
+ ***********************************************************************/
11
+ import { describe, test, expect } from "vitest";
12
+ import {
13
+ fold_build_model,
14
+ fold_new_state,
15
+ fold_visible_set,
16
+ fold_expand_to_depth,
17
+ fold_expand_all,
18
+ fold_collapse_all,
19
+ fold_toggle,
20
+ fold_show_more,
21
+ fold_reveal,
22
+ fold_pills_of,
23
+ fold_pending_groups,
24
+ fold_group_key,
25
+ fold_root_group_key,
26
+ fold_split_group_key,
27
+ } from "./treedb_fold_model.js";
28
+
29
+ const places_desc = {
30
+ topic_name: "places",
31
+ node_treedb_type: "hierarchical",
32
+ cols: [
33
+ {id: "id", flag: []},
34
+ {id: "place", flag: ["fkey"], fkey: {places: "places"}},
35
+ {id: "places", flag: ["hook"], hook: {places: "place"}},
36
+ {id: "devices", flag: ["hook"], hook: {devices: "place"}},
37
+ {id: "controllers", flag: ["hook"], hook: {controllers: "place"}},
38
+ ],
39
+ };
40
+ const devices_desc = {
41
+ topic_name: "devices",
42
+ node_treedb_type: "child",
43
+ cols: [
44
+ {id: "id", flag: []},
45
+ {id: "place", flag: ["fkey"], fkey: {places: "devices"}},
46
+ {id: "controller", flag: ["fkey"], fkey: {controllers: "devices"}},
47
+ {id: "foto", flag: ["fkey", "file"], fkey: {__assets__: "fotos"}},
48
+ ],
49
+ };
50
+ const controllers_desc = {
51
+ topic_name: "controllers",
52
+ node_treedb_type: "hierarchical",
53
+ cols: [
54
+ {id: "id", flag: []},
55
+ {id: "place", flag: ["fkey"], fkey: {places: "controllers"}},
56
+ {id: "devices", flag: ["hook"], hook: {devices: "controller"}},
57
+ ],
58
+ };
59
+ const groups_desc = {
60
+ topic_name: "device_groups",
61
+ node_treedb_type: "extended",
62
+ cols: [
63
+ {id: "id", flag: []},
64
+ {id: "members", flag: ["hook"], hook: {devices: "group"}},
65
+ ],
66
+ };
67
+
68
+ const descs = {
69
+ __graphs__: {topic_name: "__graphs__", cols: []},
70
+ places: places_desc,
71
+ devices: devices_desc,
72
+ controllers: controllers_desc,
73
+ device_groups: groups_desc,
74
+ };
75
+
76
+ /* es > (norte, sur); norte > nave1; nave1 has 30 devices and one
77
+ * controller c1 that also reaches devices d0..d9. */
78
+ function make_records(n_devices)
79
+ {
80
+ let devices = [];
81
+ for(let i = 0; i < n_devices; i++) {
82
+ let rec = {id: `d${i}`, place: "places^nave1^devices", foto: "x^y^fotos"};
83
+ if(i < 10) {
84
+ rec.controller = ["controllers^c1^devices"];
85
+ }
86
+ devices.push(rec);
87
+ }
88
+ return {
89
+ places: [
90
+ {id: "es", place: ""},
91
+ {id: "norte", place: "places^es^places"},
92
+ {id: "sur", place: "places^es^places"},
93
+ {id: "nave1", place: "places^norte^places"},
94
+ ],
95
+ devices: devices,
96
+ controllers: [{id: "c1", place: "places^nave1^controllers"}],
97
+ device_groups: [{id: "g1"}],
98
+ };
99
+ }
100
+
101
+ const K = (t, id) => `${t}^${id}`;
102
+
103
+ describe("the model", () => {
104
+ test("every record is a node, system topics are skipped", () => {
105
+ let m = fold_build_model(descs, make_records(3));
106
+ expect(m.nodes.size).toBe(4 + 3 + 1 + 1);
107
+ expect(m.topics).toEqual(["places", "devices", "controllers", "device_groups"]);
108
+ });
109
+
110
+ test("children hang from the hook the fkey names, in record order", () => {
111
+ let m = fold_build_model(descs, make_records(3));
112
+ expect(m.groups.get(fold_group_key(K("places", "es"), "places")))
113
+ .toEqual([K("places", "norte"), K("places", "sur")]);
114
+ expect(m.groups.get(fold_group_key(K("places", "nave1"), "devices")))
115
+ .toEqual([K("devices", "d0"), K("devices", "d1"), K("devices", "d2")]);
116
+ expect(m.groups.get(fold_group_key(K("controllers", "c1"), "devices")))
117
+ .toEqual([K("devices", "d0"), K("devices", "d1"), K("devices", "d2")]);
118
+ });
119
+
120
+ test("a device has two parents and is one node", () => {
121
+ let m = fold_build_model(descs, make_records(3));
122
+ let d0 = m.nodes.get(K("devices", "d0"));
123
+ expect(d0.parents.map((p) => p.node_key).sort())
124
+ .toEqual([K("controllers", "c1"), K("places", "nave1")]);
125
+ });
126
+
127
+ test("roots: no drawable parent -- and a fkey into a topic nobody fetched is not a parent", () => {
128
+ let m = fold_build_model(descs, make_records(3));
129
+ expect(m.groups.get(fold_root_group_key("places"))).toEqual([K("places", "es")]);
130
+ expect(m.groups.get(fold_root_group_key("devices"))).toBeUndefined();
131
+ /* `foto` names __assets__, which has no desc: ignored */
132
+ expect(m.nodes.get(K("devices", "d0")).parents.length).toBe(2);
133
+ expect(m.groups.get(fold_root_group_key("device_groups"))).toEqual([K("device_groups", "g1")]);
134
+ });
135
+
136
+ test("a child of an `extended` parent only is a root: the engine draws no edge to one", () => {
137
+ let recs = make_records(1);
138
+ recs.devices[0] = {id: "d0", group: "device_groups^g1^members"};
139
+ let m = fold_build_model(descs, recs);
140
+ expect(m.groups.get(fold_root_group_key("devices"))).toEqual([K("devices", "d0")]);
141
+ expect(m.nodes.get(K("device_groups", "g1")).hooks).toEqual({});
142
+ });
143
+
144
+ test("a cycle with no way in gets one root, not a row of them", () => {
145
+ let recs = {
146
+ places: [
147
+ {id: "a", place: "places^c^places"},
148
+ {id: "b", place: "places^a^places"},
149
+ {id: "c", place: "places^b^places"},
150
+ ],
151
+ };
152
+ let m = fold_build_model({places: places_desc}, recs);
153
+ expect(m.groups.get(fold_root_group_key("places"))).toEqual([K("places", "a")]);
154
+ let st = fold_new_state(24);
155
+ fold_expand_all(m, st);
156
+ expect(fold_visible_set(m, st).size).toBe(3);
157
+ });
158
+
159
+ test("group keys split back", () => {
160
+ expect(fold_split_group_key("places^nave1|devices"))
161
+ .toEqual({node_key: "places^nave1", hook: "devices"});
162
+ expect(fold_split_group_key("|devices")).toEqual({node_key: "", hook: "devices"});
163
+ expect(fold_split_group_key("nope")).toBeNull();
164
+ });
165
+ });
166
+
167
+ describe("depth", () => {
168
+ test("depth 1 is the roots alone", () => {
169
+ let m = fold_build_model(descs, make_records(30));
170
+ let st = fold_new_state(24);
171
+ fold_expand_to_depth(m, st, 1);
172
+ expect([...fold_visible_set(m, st)].sort())
173
+ .toEqual([K("device_groups", "g1"), K("places", "es")]);
174
+ });
175
+
176
+ test("depth 2 opens the roots' hooks on their first page", () => {
177
+ let m = fold_build_model(descs, make_records(30));
178
+ let st = fold_new_state(24);
179
+ fold_expand_to_depth(m, st, 2);
180
+ let v = fold_visible_set(m, st);
181
+ expect(v.has(K("places", "norte"))).toBe(true);
182
+ expect(v.has(K("places", "sur"))).toBe(true);
183
+ expect(v.has(K("places", "nave1"))).toBe(false);
184
+ expect(v.has(K("devices", "d0"))).toBe(false);
185
+ });
186
+
187
+ test("depth 4 reaches the devices, one page of them", () => {
188
+ let m = fold_build_model(descs, make_records(30));
189
+ let st = fold_new_state(24);
190
+ fold_expand_to_depth(m, st, 4);
191
+ let v = fold_visible_set(m, st);
192
+ expect(v.has(K("devices", "d0"))).toBe(true);
193
+ expect(v.has(K("devices", "d23"))).toBe(true);
194
+ expect(v.has(K("devices", "d24"))).toBe(false);
195
+ expect(v.has(K("controllers", "c1"))).toBe(true);
196
+ });
197
+
198
+ test("expand all shows everything, collapse all the roots' first page", () => {
199
+ let m = fold_build_model(descs, make_records(30));
200
+ let st = fold_new_state(24);
201
+ fold_expand_all(m, st);
202
+ expect(fold_visible_set(m, st).size).toBe(m.nodes.size);
203
+ fold_collapse_all(m, st);
204
+ expect(fold_visible_set(m, st).size).toBe(2);
205
+ });
206
+ });
207
+
208
+ describe("toggle and pages", () => {
209
+ test("a folded group opens on its first page and closes again", () => {
210
+ let m = fold_build_model(descs, make_records(30));
211
+ let st = fold_new_state(24);
212
+ fold_expand_to_depth(m, st, 3);
213
+ let g = fold_group_key(K("places", "nave1"), "devices");
214
+ expect(fold_toggle(m, st, g)).toBe(true);
215
+ expect(fold_visible_set(m, st).has(K("devices", "d5"))).toBe(true);
216
+ expect(fold_toggle(m, st, g)).toBe(false);
217
+ expect(fold_visible_set(m, st).has(K("devices", "d5"))).toBe(false);
218
+ });
219
+
220
+ test("show more adds a page and stops at the total", () => {
221
+ let m = fold_build_model(descs, make_records(30));
222
+ let st = fold_new_state(24);
223
+ let g = fold_group_key(K("places", "nave1"), "devices");
224
+ expect(fold_show_more(m, st, g)).toBe(24);
225
+ expect(fold_show_more(m, st, g)).toBe(30);
226
+ expect(fold_show_more(m, st, g)).toBe(30);
227
+ });
228
+
229
+ test("a group open under a folded ancestor is remembered, not shown", () => {
230
+ let m = fold_build_model(descs, make_records(30));
231
+ let st = fold_new_state(24);
232
+ fold_expand_to_depth(m, st, 4);
233
+ fold_toggle(m, st, fold_group_key(K("places", "es"), "places")); /* close */
234
+ let v = fold_visible_set(m, st);
235
+ expect(v.has(K("devices", "d0"))).toBe(false);
236
+ expect(v.size).toBe(2);
237
+ fold_toggle(m, st, fold_group_key(K("places", "es"), "places")); /* reopen */
238
+ expect(fold_visible_set(m, st).has(K("devices", "d0"))).toBe(true);
239
+ });
240
+
241
+ test("the union: folding one parent leaves the child where the other shows it", () => {
242
+ let m = fold_build_model(descs, make_records(30));
243
+ let st = fold_new_state(24);
244
+ fold_expand_to_depth(m, st, 4);
245
+ let g_place = fold_group_key(K("places", "nave1"), "devices");
246
+ let g_ctrl = fold_group_key(K("controllers", "c1"), "devices");
247
+ fold_toggle(m, st, g_ctrl); /* c1's devices open too */
248
+ fold_toggle(m, st, g_place); /* nave1's devices folded */
249
+ let v = fold_visible_set(m, st);
250
+ expect(v.has(K("devices", "d3"))).toBe(true); /* via c1 */
251
+ expect(v.has(K("devices", "d15"))).toBe(false); /* only via nave1 */
252
+ });
253
+
254
+ test("pending groups: what the +N chips are for", () => {
255
+ let m = fold_build_model(descs, make_records(30));
256
+ let st = fold_new_state(24);
257
+ fold_expand_to_depth(m, st, 4);
258
+ let v = fold_visible_set(m, st);
259
+ let pending = fold_pending_groups(m, st, v);
260
+ expect(pending.length).toBe(1);
261
+ expect(pending[0]).toMatchObject({
262
+ group_key: fold_group_key(K("places", "nave1"), "devices"),
263
+ node_key: K("places", "nave1"), hook: "devices",
264
+ shown: 24, total: 30, rest: 6,
265
+ });
266
+ });
267
+
268
+ test("pills of a card: one per hook with children, coloured by the child's topic", () => {
269
+ let m = fold_build_model(descs, make_records(30));
270
+ let st = fold_new_state(24);
271
+ fold_expand_to_depth(m, st, 4);
272
+ let pills = fold_pills_of(m, st, K("places", "nave1"));
273
+ expect(pills.map((p) => p.hook)).toEqual(["devices", "controllers"]);
274
+ expect(pills[0]).toMatchObject({total: 30, shown: 24, open: true, child_topic: "devices"});
275
+ expect(pills[1]).toMatchObject({total: 1, shown: 1, open: true, child_topic: "controllers"});
276
+ expect(fold_pills_of(m, st, K("places", "sur"))).toEqual([]);
277
+ });
278
+ });
279
+
280
+ describe("reveal", () => {
281
+ test("opens the path down to a hidden node, one page boundary at a time", () => {
282
+ let m = fold_build_model(descs, make_records(60));
283
+ let st = fold_new_state(24);
284
+ fold_expand_to_depth(m, st, 1);
285
+ expect(fold_reveal(m, st, K("devices", "d40"))).toBe(true);
286
+ let v = fold_visible_set(m, st);
287
+ expect(v.has(K("devices", "d40"))).toBe(true);
288
+ /* d40 is the 41st: two pages, not three, and not all */
289
+ expect(v.has(K("devices", "d47"))).toBe(true);
290
+ expect(v.has(K("devices", "d48"))).toBe(false);
291
+ expect(v.has(K("places", "sur"))).toBe(true); /* es|places opened whole page */
292
+ });
293
+
294
+ test("does not close what was open", () => {
295
+ let m = fold_build_model(descs, make_records(60));
296
+ let st = fold_new_state(24);
297
+ fold_expand_to_depth(m, st, 4);
298
+ fold_show_more(m, st, fold_group_key(K("places", "nave1"), "devices")); /* 48 */
299
+ fold_reveal(m, st, K("devices", "d3"));
300
+ expect(fold_visible_set(m, st).has(K("devices", "d47"))).toBe(true);
301
+ });
302
+
303
+ test("an unknown key is refused", () => {
304
+ let m = fold_build_model(descs, make_records(3));
305
+ let st = fold_new_state(24);
306
+ expect(fold_reveal(m, st, K("devices", "nope"))).toBe(false);
307
+ });
308
+ });