@yuneta/gobj-ui 7.23.29 → 7.23.30
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/README.md +163 -2
- package/dist/gobj-ui.cjs.js +950 -179
- package/dist/gobj-ui.es.js +951 -180
- package/package.json +1 -1
- package/src/c_yui_gobj_tree_js.js +435 -245
- package/src/gclass_describe.js +234 -0
- package/src/gclass_describe.test.js +125 -0
- package/src/gobj_tree_model.js +436 -0
- package/src/gobj_tree_model.test.js +184 -0
- package/src/yui_gclass_view.js +277 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* gobj_tree_model.js
|
|
3
|
+
*
|
|
4
|
+
* The gobj tree of a yuno, as plain data.
|
|
5
|
+
*
|
|
6
|
+
* The view (C_YUI_GOBJ_TREE_JS) draws NODE DESCRIPTORS, never
|
|
7
|
+
* gobjs. There are two producers of a descriptor, because the
|
|
8
|
+
* view has to show the gobjs of the BACKEND too:
|
|
9
|
+
*
|
|
10
|
+
* - describe_js_gobj() the gobj tree of THIS browser yuno
|
|
11
|
+
* - describe_backend_tree() the answer of the backend command
|
|
12
|
+
* `view-gobj-tree` of another yuno
|
|
13
|
+
*
|
|
14
|
+
* Both answer the SAME shape, and that shape is the backend's:
|
|
15
|
+
* the field names are the ones `gobj2json()` writes in
|
|
16
|
+
* `kernel/c/gobj-c/src/gobj.c` (fullname, shortname, gclass,
|
|
17
|
+
* name, parent, state, running, playing, service, disabled,
|
|
18
|
+
* volatil, bottom_gobj, gobj_flags, commands, gobj_trace_level).
|
|
19
|
+
* Anything the browser runtime cannot answer is simply absent,
|
|
20
|
+
* and every reader here treats absent as "not known", never as
|
|
21
|
+
* false.
|
|
22
|
+
*
|
|
23
|
+
* Copyright (c) 2026, ArtGins.
|
|
24
|
+
* All Rights Reserved.
|
|
25
|
+
***********************************************************************/
|
|
26
|
+
import {
|
|
27
|
+
gobj_gclass_name,
|
|
28
|
+
gobj_name,
|
|
29
|
+
gobj_short_name,
|
|
30
|
+
gobj_full_name,
|
|
31
|
+
gobj_parent,
|
|
32
|
+
gobj_current_state,
|
|
33
|
+
gobj_is_running,
|
|
34
|
+
gobj_is_playing,
|
|
35
|
+
gobj_is_service,
|
|
36
|
+
gobj_is_pure_child,
|
|
37
|
+
gobj_is_volatil,
|
|
38
|
+
gobj_bottom_gobj,
|
|
39
|
+
} from "@yuneta/gobj-js";
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/******************************
|
|
43
|
+
* Roles and status
|
|
44
|
+
******************************/
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/* What a node IS. One value: a gobj is drawn once, so it gets one
|
|
48
|
+
* colour. The order is the order of specificity, and the yuno wins
|
|
49
|
+
* over everything because the root is the yuno by construction. */
|
|
50
|
+
const ROLES = ["yuno", "service", "volatil", "pure", "child"];
|
|
51
|
+
|
|
52
|
+
/* What a node is DOING. `disabled` is not one of these -- a disabled
|
|
53
|
+
* gobj is still stopped or running, so it travels as its own mark. */
|
|
54
|
+
const STATUSES = ["stopped", "running", "playing"];
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/************************************************************
|
|
58
|
+
* The role of a node: "yuno" | "service" | "volatil" | "pure" | "child"
|
|
59
|
+
************************************************************/
|
|
60
|
+
function node_role(d)
|
|
61
|
+
{
|
|
62
|
+
if(!d) {
|
|
63
|
+
return "child";
|
|
64
|
+
}
|
|
65
|
+
if(d.is_yuno) {
|
|
66
|
+
return "yuno";
|
|
67
|
+
}
|
|
68
|
+
if(d.service) {
|
|
69
|
+
return "service";
|
|
70
|
+
}
|
|
71
|
+
if(d.volatil) {
|
|
72
|
+
return "volatil";
|
|
73
|
+
}
|
|
74
|
+
if(d.pure_child) {
|
|
75
|
+
return "pure";
|
|
76
|
+
}
|
|
77
|
+
return "child";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/************************************************************
|
|
81
|
+
* What the node is doing: "stopped" | "running" | "playing"
|
|
82
|
+
************************************************************/
|
|
83
|
+
function node_status(d)
|
|
84
|
+
{
|
|
85
|
+
if(!d || !d.running) {
|
|
86
|
+
return "stopped";
|
|
87
|
+
}
|
|
88
|
+
return d.playing? "playing": "running";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/************************************************************
|
|
92
|
+
* The i18n key of the status, which is NOT the status itself:
|
|
93
|
+
* a gobj that runs and does not play is PAUSED to the reader,
|
|
94
|
+
* and "running" alone never said which of the two it was.
|
|
95
|
+
************************************************************/
|
|
96
|
+
function node_status_key(d)
|
|
97
|
+
{
|
|
98
|
+
switch(node_status(d)) {
|
|
99
|
+
case "playing":
|
|
100
|
+
return "playing";
|
|
101
|
+
case "running":
|
|
102
|
+
return "running (paused)";
|
|
103
|
+
default:
|
|
104
|
+
return "stopped";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/******************************
|
|
110
|
+
* Producers
|
|
111
|
+
******************************/
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
/************************************************************
|
|
115
|
+
* A descriptor of a LIVE gobj of this browser yuno, children
|
|
116
|
+
* included (the whole subtree, depth-first).
|
|
117
|
+
*
|
|
118
|
+
* `is_root` marks the yuno itself: the JS runtime has no
|
|
119
|
+
* gobj_flag_yuno test of its own.
|
|
120
|
+
************************************************************/
|
|
121
|
+
function describe_js_gobj(target_gobj, is_root)
|
|
122
|
+
{
|
|
123
|
+
if(!target_gobj) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let parent_gobj = gobj_parent(target_gobj);
|
|
128
|
+
let bottom = gobj_bottom_gobj(target_gobj);
|
|
129
|
+
|
|
130
|
+
let d = {
|
|
131
|
+
gclass: gobj_gclass_name(target_gobj) || "",
|
|
132
|
+
name: gobj_name(target_gobj) || "",
|
|
133
|
+
shortname: gobj_short_name(target_gobj) || "",
|
|
134
|
+
fullname: gobj_full_name(target_gobj) || "",
|
|
135
|
+
parent: parent_gobj? (gobj_short_name(parent_gobj) || ""): "",
|
|
136
|
+
state: gobj_current_state(target_gobj) || "",
|
|
137
|
+
running: !!gobj_is_running(target_gobj),
|
|
138
|
+
playing: !!gobj_is_playing(target_gobj),
|
|
139
|
+
service: !!gobj_is_service(target_gobj),
|
|
140
|
+
volatil: !!gobj_is_volatil(target_gobj),
|
|
141
|
+
pure_child: !!gobj_is_pure_child(target_gobj),
|
|
142
|
+
|
|
143
|
+
/* gobj-js exports no gobj_is_disabled() (the function exists,
|
|
144
|
+
* the export list does not carry it), so the flag is read from
|
|
145
|
+
* the instance the way gobj_enable/gobj_disable write it. The
|
|
146
|
+
* backend answers the same name in `view-gobj-tree`. */
|
|
147
|
+
disabled: !!target_gobj.disabled,
|
|
148
|
+
|
|
149
|
+
bottom_gobj: bottom? (gobj_short_name(bottom) || ""): "",
|
|
150
|
+
commands: !!(target_gobj.gclass && target_gobj.gclass.command_table),
|
|
151
|
+
is_yuno: !!is_root,
|
|
152
|
+
children: [],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let children = target_gobj.dl_children || [];
|
|
156
|
+
for(let child of children) {
|
|
157
|
+
let cd = describe_js_gobj(child, false);
|
|
158
|
+
if(cd) {
|
|
159
|
+
d.children.push(cd);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return d;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/************************************************************
|
|
167
|
+
* A descriptor from ONE node of the `view-gobj-tree` answer
|
|
168
|
+
* of a backend yuno.
|
|
169
|
+
*
|
|
170
|
+
* `shortname` is the key the node hangs from in that answer;
|
|
171
|
+
* the node itself repeats it, so the argument is only the
|
|
172
|
+
* fallback for a filtered answer that dropped the field.
|
|
173
|
+
************************************************************/
|
|
174
|
+
function describe_backend_node(shortname, node, is_root)
|
|
175
|
+
{
|
|
176
|
+
if(!node || typeof node !== "object") {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let flags = Array.isArray(node.gobj_flags)? node.gobj_flags: [];
|
|
181
|
+
let has = (flag) => flags.indexOf(flag) !== -1;
|
|
182
|
+
|
|
183
|
+
let d = {
|
|
184
|
+
gclass: node.gclass || "",
|
|
185
|
+
name: node.name || "",
|
|
186
|
+
shortname: node.shortname || shortname || "",
|
|
187
|
+
fullname: node.fullname || "",
|
|
188
|
+
parent: node.parent || "",
|
|
189
|
+
state: node.state || "",
|
|
190
|
+
running: !!node.running,
|
|
191
|
+
playing: !!node.playing,
|
|
192
|
+
service: !!node.service || has("gobj_flag_service") ||
|
|
193
|
+
has("gobj_flag_default_service"),
|
|
194
|
+
volatil: !!node.volatil || has("gobj_flag_volatil"),
|
|
195
|
+
pure_child: has("gobj_flag_pure_child"),
|
|
196
|
+
disabled: !!node.disabled,
|
|
197
|
+
bottom_gobj: node.bottom_gobj || "",
|
|
198
|
+
commands: !!node.commands,
|
|
199
|
+
gobj_flags: flags,
|
|
200
|
+
gobj_trace_level: Array.isArray(node.gobj_trace_level)?
|
|
201
|
+
node.gobj_trace_level: [],
|
|
202
|
+
is_yuno: (is_root !== undefined)?
|
|
203
|
+
!!is_root: has("gobj_flag_yuno"),
|
|
204
|
+
children: [],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
let children = (node.children && typeof node.children === "object")?
|
|
208
|
+
node.children: null;
|
|
209
|
+
if(children) {
|
|
210
|
+
for(let key of Object.keys(children)) {
|
|
211
|
+
let cd = describe_backend_node(key, children[key], false);
|
|
212
|
+
if(cd) {
|
|
213
|
+
d.children.push(cd);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return d;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/************************************************************
|
|
222
|
+
* The whole `view-gobj-tree` answer -> the root descriptor.
|
|
223
|
+
*
|
|
224
|
+
* That answer is a dict of ONE key (the shortname of the gobj
|
|
225
|
+
* the command was asked about) even though it is shaped as a
|
|
226
|
+
* dict all the way down. Returns null for anything else, and
|
|
227
|
+
* the caller reports it: an empty tree and a malformed answer
|
|
228
|
+
* must not look the same.
|
|
229
|
+
************************************************************/
|
|
230
|
+
function describe_backend_tree(data)
|
|
231
|
+
{
|
|
232
|
+
if(!data || typeof data !== "object" || Array.isArray(data)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
let keys = Object.keys(data);
|
|
236
|
+
if(keys.length === 0) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return describe_backend_node(keys[0], data[keys[0]], true);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
/******************************
|
|
244
|
+
* Readouts
|
|
245
|
+
******************************/
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
/************************************************************
|
|
249
|
+
* The i18n key of a role. Two of the five roles are not the
|
|
250
|
+
* word the reader wants ("pure" and "volatil" alone say
|
|
251
|
+
* nothing), so the map is explicit.
|
|
252
|
+
************************************************************/
|
|
253
|
+
function role_i18n_key(role)
|
|
254
|
+
{
|
|
255
|
+
switch(role) {
|
|
256
|
+
case "yuno":
|
|
257
|
+
return "yuno";
|
|
258
|
+
case "service":
|
|
259
|
+
return "service";
|
|
260
|
+
case "volatil":
|
|
261
|
+
return "volatil child";
|
|
262
|
+
case "pure":
|
|
263
|
+
return "pure child";
|
|
264
|
+
default:
|
|
265
|
+
return "child";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/************************************************************
|
|
270
|
+
* The marks a node carries besides its role: what the reader
|
|
271
|
+
* would otherwise have to open the popover to learn.
|
|
272
|
+
*
|
|
273
|
+
* Returns i18n KEYS, in a fixed order, so the card and the
|
|
274
|
+
* popover cannot disagree about what a node is.
|
|
275
|
+
************************************************************/
|
|
276
|
+
function node_badge_keys(d)
|
|
277
|
+
{
|
|
278
|
+
let badges = [];
|
|
279
|
+
if(!d) {
|
|
280
|
+
return badges;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let role = node_role(d);
|
|
284
|
+
if(role !== "child") {
|
|
285
|
+
badges.push(role_i18n_key(role));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if(d.disabled) {
|
|
289
|
+
badges.push("disabled");
|
|
290
|
+
}
|
|
291
|
+
if(d.bottom_gobj) {
|
|
292
|
+
badges.push("bottom");
|
|
293
|
+
}
|
|
294
|
+
if(d.commands) {
|
|
295
|
+
badges.push("commands");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return badges;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/************************************************************
|
|
302
|
+
* The vocabulary of this view, translated.
|
|
303
|
+
*
|
|
304
|
+
* Written as literal t("...") calls and not as t(key): a key
|
|
305
|
+
* reached through a variable is invisible to the locale
|
|
306
|
+
* validator, which is how a fixed vocabulary ships in one
|
|
307
|
+
* language and never changes it.
|
|
308
|
+
************************************************************/
|
|
309
|
+
function node_labels(t)
|
|
310
|
+
{
|
|
311
|
+
if(typeof t !== "function") {
|
|
312
|
+
t = (k) => k;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
role: {
|
|
316
|
+
"yuno": t("yuno"),
|
|
317
|
+
"service": t("service"),
|
|
318
|
+
"volatil child": t("volatil child"),
|
|
319
|
+
"pure child": t("pure child"),
|
|
320
|
+
"child": t("child"),
|
|
321
|
+
},
|
|
322
|
+
/* The precise words, for the popover, which has room. */
|
|
323
|
+
status: {
|
|
324
|
+
"playing": t("playing"),
|
|
325
|
+
"running (paused)": t("running (paused)"),
|
|
326
|
+
"stopped": t("stopped"),
|
|
327
|
+
},
|
|
328
|
+
/* The glanceable ones, for the pill on the card, which has
|
|
329
|
+
* not: "Ejecutando (en pausa)" pushes the FSM state off its
|
|
330
|
+
* own row. The colour carries the rest, and the pill's
|
|
331
|
+
* `title` says it in full. */
|
|
332
|
+
status_short: {
|
|
333
|
+
"playing": t("playing"),
|
|
334
|
+
"running": t("running"),
|
|
335
|
+
"stopped": t("stopped"),
|
|
336
|
+
},
|
|
337
|
+
badge: {
|
|
338
|
+
"yuno": t("yuno"),
|
|
339
|
+
"service": t("service"),
|
|
340
|
+
"volatil child": t("volatil child"),
|
|
341
|
+
"pure child": t("pure child"),
|
|
342
|
+
"disabled": t("disabled"),
|
|
343
|
+
"bottom": t("bottom"),
|
|
344
|
+
"commands": t("commands"),
|
|
345
|
+
},
|
|
346
|
+
field: {
|
|
347
|
+
gclass: t("gclass"),
|
|
348
|
+
name: t("name"),
|
|
349
|
+
full_name: t("full name"),
|
|
350
|
+
role: t("role"),
|
|
351
|
+
status: t("status"),
|
|
352
|
+
state: t("state"),
|
|
353
|
+
disabled: t("disabled"),
|
|
354
|
+
parent: t("parent"),
|
|
355
|
+
bottom_gobj: t("bottom gobj"),
|
|
356
|
+
commands: t("commands"),
|
|
357
|
+
traces: t("traces"),
|
|
358
|
+
flags: t("flags"),
|
|
359
|
+
children: t("children"),
|
|
360
|
+
yes: t("yes"),
|
|
361
|
+
collapsed: t("(collapsed)"),
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/************************************************************
|
|
367
|
+
* The rows of the popover: [label, value], both translated.
|
|
368
|
+
*
|
|
369
|
+
* `t` is the app's translator -- the library has none of its
|
|
370
|
+
* own. Values that are a fixed vocabulary (the role, the
|
|
371
|
+
* status, yes) go through it too; free text does not.
|
|
372
|
+
************************************************************/
|
|
373
|
+
function node_info_rows(d, t)
|
|
374
|
+
{
|
|
375
|
+
let rows = [];
|
|
376
|
+
if(!d) {
|
|
377
|
+
return rows;
|
|
378
|
+
}
|
|
379
|
+
let L = node_labels(t);
|
|
380
|
+
let f = L.field;
|
|
381
|
+
|
|
382
|
+
rows.push([f.gclass, d.gclass || "\u2014"]);
|
|
383
|
+
rows.push([f.name, d.name || "\u2014"]);
|
|
384
|
+
|
|
385
|
+
if(d.fullname && d.fullname !== d.shortname) {
|
|
386
|
+
rows.push([f.full_name, d.fullname]);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
|
|
390
|
+
rows.push([f.status, L.status[node_status_key(d)]]);
|
|
391
|
+
rows.push([f.state, d.state || "\u2014"]);
|
|
392
|
+
|
|
393
|
+
if(d.disabled) {
|
|
394
|
+
rows.push([f.disabled, f.yes]);
|
|
395
|
+
}
|
|
396
|
+
if(d.parent) {
|
|
397
|
+
rows.push([f.parent, d.parent]);
|
|
398
|
+
}
|
|
399
|
+
if(d.bottom_gobj) {
|
|
400
|
+
rows.push([f.bottom_gobj, d.bottom_gobj]);
|
|
401
|
+
}
|
|
402
|
+
if(d.commands) {
|
|
403
|
+
rows.push([f.commands, f.yes]);
|
|
404
|
+
}
|
|
405
|
+
if(Array.isArray(d.gobj_trace_level) && d.gobj_trace_level.length > 0) {
|
|
406
|
+
rows.push([f.traces, d.gobj_trace_level.join(", ")]);
|
|
407
|
+
}
|
|
408
|
+
if(Array.isArray(d.gobj_flags) && d.gobj_flags.length > 0) {
|
|
409
|
+
rows.push([f.flags, d.gobj_flags.join(", ")]);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let kids = Array.isArray(d.children)? d.children.length: 0;
|
|
413
|
+
if(kids > 0) {
|
|
414
|
+
rows.push([
|
|
415
|
+
f.children,
|
|
416
|
+
d.is_collapsed? (kids + " " + f.collapsed): String(kids)
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return rows;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export {
|
|
424
|
+
ROLES,
|
|
425
|
+
STATUSES,
|
|
426
|
+
node_role,
|
|
427
|
+
node_status,
|
|
428
|
+
node_status_key,
|
|
429
|
+
node_badge_keys,
|
|
430
|
+
node_labels,
|
|
431
|
+
node_info_rows,
|
|
432
|
+
role_i18n_key,
|
|
433
|
+
describe_js_gobj,
|
|
434
|
+
describe_backend_node,
|
|
435
|
+
describe_backend_tree,
|
|
436
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
node_role,
|
|
4
|
+
node_status,
|
|
5
|
+
node_status_key,
|
|
6
|
+
node_badge_keys,
|
|
7
|
+
node_info_rows,
|
|
8
|
+
describe_backend_node,
|
|
9
|
+
describe_backend_tree,
|
|
10
|
+
} from "./gobj_tree_model.js";
|
|
11
|
+
|
|
12
|
+
/* A `view-gobj-tree` answer, trimmed to what the view reads. */
|
|
13
|
+
const BACKEND_TREE = {
|
|
14
|
+
"Yuno^my_yuno": {
|
|
15
|
+
fullname: "Yuno^my_yuno",
|
|
16
|
+
shortname: "Yuno^my_yuno",
|
|
17
|
+
gclass: "C_YUNO",
|
|
18
|
+
name: "my_yuno",
|
|
19
|
+
parent: "",
|
|
20
|
+
state: "ST_IDLE",
|
|
21
|
+
running: true,
|
|
22
|
+
playing: true,
|
|
23
|
+
service: true,
|
|
24
|
+
disabled: false,
|
|
25
|
+
volatil: false,
|
|
26
|
+
commands: true,
|
|
27
|
+
gobj_flags: ["gobj_flag_yuno", "gobj_flag_service"],
|
|
28
|
+
gobj_trace_level: ["machine"],
|
|
29
|
+
children: {
|
|
30
|
+
"C_TIMER^timer": {
|
|
31
|
+
shortname: "C_TIMER^timer",
|
|
32
|
+
gclass: "C_TIMER",
|
|
33
|
+
name: "timer",
|
|
34
|
+
parent: "Yuno^my_yuno",
|
|
35
|
+
state: "ST_IDLE",
|
|
36
|
+
running: true,
|
|
37
|
+
playing: false,
|
|
38
|
+
service: false,
|
|
39
|
+
disabled: false,
|
|
40
|
+
volatil: false,
|
|
41
|
+
gobj_flags: ["gobj_flag_pure_child"],
|
|
42
|
+
},
|
|
43
|
+
"C_TCP^conn": {
|
|
44
|
+
shortname: "C_TCP^conn",
|
|
45
|
+
gclass: "C_TCP",
|
|
46
|
+
name: "conn",
|
|
47
|
+
state: "ST_STOPPED",
|
|
48
|
+
running: false,
|
|
49
|
+
playing: false,
|
|
50
|
+
service: false,
|
|
51
|
+
disabled: true,
|
|
52
|
+
volatil: true,
|
|
53
|
+
bottom_gobj: "C_TCP^raw",
|
|
54
|
+
gobj_flags: ["gobj_flag_volatil"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
describe("describe_backend_tree", () => {
|
|
61
|
+
it("reads the root and its children out of the command answer", () => {
|
|
62
|
+
let root = describe_backend_tree(BACKEND_TREE);
|
|
63
|
+
expect(root.gclass).toBe("C_YUNO");
|
|
64
|
+
expect(root.is_yuno).toBe(true);
|
|
65
|
+
expect(root.children.length).toBe(2);
|
|
66
|
+
expect(root.children.map(c => c.gclass).sort())
|
|
67
|
+
.toEqual(["C_TCP", "C_TIMER"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("a malformed answer is null, not an empty tree", () => {
|
|
71
|
+
expect(describe_backend_tree(null)).toBe(null);
|
|
72
|
+
expect(describe_backend_tree({})).toBe(null);
|
|
73
|
+
expect(describe_backend_tree([])).toBe(null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("takes pure_child from gobj_flags, which has no boolean of its own", () => {
|
|
77
|
+
let root = describe_backend_tree(BACKEND_TREE);
|
|
78
|
+
let timer = root.children.find(c => c.gclass === "C_TIMER");
|
|
79
|
+
expect(timer.pure_child).toBe(true);
|
|
80
|
+
expect(timer.service).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("a node with no children answers an empty list, not undefined", () => {
|
|
84
|
+
let d = describe_backend_node("C_X^x", {gclass: "C_X"}, false);
|
|
85
|
+
expect(d.children).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("node_role", () => {
|
|
90
|
+
it("the yuno wins over every other mark", () => {
|
|
91
|
+
expect(node_role({is_yuno: true, service: true, volatil: true}))
|
|
92
|
+
.toBe("yuno");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("orders service over volatil over pure", () => {
|
|
96
|
+
expect(node_role({service: true, volatil: true})).toBe("service");
|
|
97
|
+
expect(node_role({volatil: true, pure_child: true})).toBe("volatil");
|
|
98
|
+
expect(node_role({pure_child: true})).toBe("pure");
|
|
99
|
+
expect(node_role({})).toBe("child");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("node_status", () => {
|
|
104
|
+
it("tells the three states apart", () => {
|
|
105
|
+
expect(node_status({running: false, playing: false})).toBe("stopped");
|
|
106
|
+
expect(node_status({running: true, playing: false})).toBe("running");
|
|
107
|
+
expect(node_status({running: true, playing: true})).toBe("playing");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("says PAUSED for the one that runs and does not play", () => {
|
|
111
|
+
expect(node_status_key({running: true, playing: false}))
|
|
112
|
+
.toBe("running (paused)");
|
|
113
|
+
expect(node_status_key({running: true, playing: true}))
|
|
114
|
+
.toBe("playing");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("a gobj that plays without running cannot happen, and reads stopped", () => {
|
|
118
|
+
expect(node_status({running: false, playing: true})).toBe("stopped");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("node_badge_keys", () => {
|
|
123
|
+
it("marks the role and every extra fact, in a fixed order", () => {
|
|
124
|
+
let root = describe_backend_tree(BACKEND_TREE);
|
|
125
|
+
expect(node_badge_keys(root)).toEqual(["yuno", "commands"]);
|
|
126
|
+
|
|
127
|
+
let tcp = root.children.find(c => c.gclass === "C_TCP");
|
|
128
|
+
expect(node_badge_keys(tcp))
|
|
129
|
+
.toEqual(["volatil child", "disabled", "bottom"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("a plain child carries no role badge", () => {
|
|
133
|
+
expect(node_badge_keys({})).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("node_info_rows", () => {
|
|
138
|
+
const rows_of = (d) => {
|
|
139
|
+
let map = {};
|
|
140
|
+
for(let [label, value] of node_info_rows(d, null)) {
|
|
141
|
+
map[label] = value;
|
|
142
|
+
}
|
|
143
|
+
return map;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
it("names the role and the status with the reader's words", () => {
|
|
147
|
+
let root = describe_backend_tree(BACKEND_TREE);
|
|
148
|
+
let timer = root.children.find(c => c.gclass === "C_TIMER");
|
|
149
|
+
let r = rows_of(timer);
|
|
150
|
+
expect(r["role"]).toBe("pure child");
|
|
151
|
+
expect(r["status"]).toBe("running (paused)");
|
|
152
|
+
expect(r["state"]).toBe("ST_IDLE");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("shows the facts a card has no room for", () => {
|
|
156
|
+
let root = describe_backend_tree(BACKEND_TREE);
|
|
157
|
+
let tcp = root.children.find(c => c.gclass === "C_TCP");
|
|
158
|
+
let r = rows_of(tcp);
|
|
159
|
+
expect(r["disabled"]).toBe("yes");
|
|
160
|
+
expect(r["bottom gobj"]).toBe("C_TCP^raw");
|
|
161
|
+
expect(r["status"]).toBe("stopped");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("carries the traces and the flags the backend reports", () => {
|
|
165
|
+
let r = rows_of(describe_backend_tree(BACKEND_TREE));
|
|
166
|
+
expect(r["traces"]).toBe("machine");
|
|
167
|
+
expect(r["flags"]).toBe("gobj_flag_yuno, gobj_flag_service");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("omits every row whose fact is absent", () => {
|
|
171
|
+
let r = rows_of(describe_backend_node("C_X^x", {gclass: "C_X"}, false));
|
|
172
|
+
expect(r["bottom gobj"]).toBeUndefined();
|
|
173
|
+
expect(r["traces"]).toBeUndefined();
|
|
174
|
+
expect(r["disabled"]).toBeUndefined();
|
|
175
|
+
expect(r["children"]).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("says how many children are hidden when the node is folded", () => {
|
|
179
|
+
let root = describe_backend_tree(BACKEND_TREE);
|
|
180
|
+
expect(rows_of(root)["children"]).toBe("2");
|
|
181
|
+
root.is_collapsed = true;
|
|
182
|
+
expect(rows_of(root)["children"]).toBe("2 (collapsed)");
|
|
183
|
+
});
|
|
184
|
+
});
|