@yuneta/gobj-ui 7.23.35 → 7.23.37

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,310 @@
1
+ import {describe, it, expect} from "vitest";
2
+
3
+ import {
4
+ normalize_flags,
5
+ normalize_type,
6
+ normalize_default,
7
+ normalize_commands,
8
+ normalize_trace_levels,
9
+ build_fsm,
10
+ gclass_view_model,
11
+ } from "./gclass_view_model.js";
12
+
13
+
14
+ /*
15
+ * C_IEVENT_CLI as the C kernel answers it (`gclass2json()`), cut to
16
+ * what the model reads. Its FSM is the interesting one: three states,
17
+ * an output event handled nowhere, and ST_SESSION reached only from
18
+ * inside an action with gobj_change_state().
19
+ */
20
+ const C_DIALECT = {
21
+ id: "C_IEVENT_CLI",
22
+ gcflag: [],
23
+ priv_size: 312,
24
+ attrs: [
25
+ {id: "url", type: "string", default_value: "", flag: "SDF_PERSIST",
26
+ description: "Url to connect"},
27
+ {id: "timeout_idack", type: "integer", default_value: "5000", flag: "SDF_RD",
28
+ description: "timeout waiting idAck"},
29
+ {id: "subscriber", type: "pointer", default_value: "", flag: "",
30
+ description: "subscriber of output-events"},
31
+ ],
32
+ commands: [],
33
+ gclass_methods: ["mt_create", "mt_start", "mt_stop"],
34
+ internal_methods: [],
35
+ info_gclass_trace: {
36
+ "ievents": "Trace inter-events with metadata of kw",
37
+ "identity-card": "Trace identity_card messages",
38
+ },
39
+ instances: 4,
40
+ FSM: {
41
+ events: [
42
+ {event_name: "EV_ON_MESSAGE", event_flag: ""},
43
+ {event_name: "EV_ON_OPEN", event_flag: "EVF_OUTPUT_EVENT|EVF_NO_WARN_SUBS"},
44
+ {event_name: "EV_ON_CLOSE", event_flag: "EVF_OUTPUT_EVENT|EVF_NO_WARN_SUBS"},
45
+ {event_name: "EV_ON_ID_NAK", event_flag: "EVF_OUTPUT_EVENT"},
46
+ ],
47
+ states: {
48
+ "ST_DISCONNECTED": [
49
+ {event_name: "EV_ON_OPEN", action: "action",
50
+ next_state: "ST_WAIT_IDENTITY_CARD_ACK"},
51
+ ],
52
+ "ST_WAIT_IDENTITY_CARD_ACK": [
53
+ {event_name: "EV_ON_MESSAGE", action: "action", next_state: ""},
54
+ {event_name: "EV_ON_CLOSE", action: "action",
55
+ next_state: "ST_DISCONNECTED"},
56
+ ],
57
+ "ST_SESSION": [
58
+ {event_name: "EV_ON_MESSAGE", action: "action", next_state: ""},
59
+ {event_name: "EV_ON_CLOSE", action: "action",
60
+ next_state: "ST_DISCONNECTED"},
61
+ ],
62
+ },
63
+ },
64
+ };
65
+
66
+ /* The same gclass as the browser registry describes it. */
67
+ const JS_DIALECT = {
68
+ id: "C_YUI_JSON",
69
+ gcflag: ["gcflag_no_check_output_events"],
70
+ attrs: [
71
+ {id: "title", type: "DTP_STRING", flag: ["SDF_RD"], default_value: "",
72
+ description: "Optional header title"},
73
+ {id: "$container", type: "DTP_POINTER", flag: [], default_value: null,
74
+ description: "HTMLElement root"},
75
+ ],
76
+ commands: [
77
+ {id: "help", alias: ["h"], description: "Command's help",
78
+ parameters: [
79
+ {id: "cmd", type: "DTP_STRING", flag: [], default_value: "",
80
+ description: "The command"},
81
+ ]},
82
+ ],
83
+ gclass_methods: ["mt_create", "mt_start"],
84
+ internal_methods: [],
85
+ trace_levels: [{name: "rows", description: "Trace rendered rows"}],
86
+ instances: 0,
87
+ FSM: {
88
+ events: [
89
+ {event_name: "EV_SET_JSON", event_flag: []},
90
+ {event_name: "EV_EXPAND_PATH",
91
+ event_flag: ["EVF_OUTPUT_EVENT", "EVF_NO_WARN_SUBS"]},
92
+ ],
93
+ states: {
94
+ "ST_EMPTY": [
95
+ {event_name: "EV_SET_JSON", action: "ac_set_json",
96
+ next_state: "ST_READY"},
97
+ ],
98
+ "ST_READY": [
99
+ {event_name: "EV_SET_JSON", action: "ac_set_json", next_state: ""},
100
+ ],
101
+ },
102
+ },
103
+ };
104
+
105
+
106
+ describe("normalize_flags", () => {
107
+ it("splits the C string form", () => {
108
+ expect(normalize_flags("SDF_WR|SDF_PERSIST")).toEqual(["SDF_WR", "SDF_PERSIST"]);
109
+ });
110
+ it("keeps the browser array form", () => {
111
+ expect(normalize_flags(["SDF_RD"])).toEqual(["SDF_RD"]);
112
+ });
113
+ it("answers a list for nothing at all", () => {
114
+ expect(normalize_flags("")).toEqual([]);
115
+ expect(normalize_flags(undefined)).toEqual([]);
116
+ expect(normalize_flags(0)).toEqual([]);
117
+ });
118
+ });
119
+
120
+ describe("normalize_type", () => {
121
+ it("shortens the browser form", () => {
122
+ expect(normalize_type("DTP_STRING")).toBe("string");
123
+ });
124
+ it("lowers the C form", () => {
125
+ expect(normalize_type("integer")).toBe("integer");
126
+ });
127
+ it("reports an unknown type as what it is", () => {
128
+ expect(normalize_type(17)).toBe("17");
129
+ expect(normalize_type(undefined)).toBe("");
130
+ });
131
+ });
132
+
133
+ describe("normalize_default", () => {
134
+ it("spells out a null pointer instead of drawing an empty cell", () => {
135
+ expect(normalize_default(null)).toBe("null");
136
+ });
137
+ it("leaves a missing default empty", () => {
138
+ expect(normalize_default(undefined)).toBe("");
139
+ });
140
+ it("prints a json default", () => {
141
+ expect(normalize_default({a: 1})).toBe('{"a":1}');
142
+ });
143
+ });
144
+
145
+ describe("normalize_commands", () => {
146
+ it("reads the C command key and the browser id key", () => {
147
+ expect(normalize_commands([{command: "view-channels"}])[0].id)
148
+ .toBe("view-channels");
149
+ expect(normalize_commands([{id: "help"}])[0].id).toBe("help");
150
+ });
151
+
152
+ it("turns a nameless C row into a section heading", () => {
153
+ const rows = normalize_commands([
154
+ {command: "help", description: "Command's help"},
155
+ {command: "", description: "\nOperation\n-----------"},
156
+ {command: "enable-channel", description: "Enable channel."},
157
+ ]);
158
+ expect(rows.map((r) => r.kind))
159
+ .toEqual(["command", "section", "command"]);
160
+ expect(rows[1].label).toBe("Operation");
161
+ });
162
+
163
+ it("drops a nameless row that says nothing", () => {
164
+ expect(normalize_commands([{command: "", description: ""}])).toEqual([]);
165
+ });
166
+
167
+ it("reads parameters from either key", () => {
168
+ const c = normalize_commands([
169
+ {command: "x", parameters: [{parameter: "channel_name", type: "string"}]},
170
+ ]);
171
+ expect(c[0].parameters[0].id).toBe("channel_name");
172
+ const js = normalize_commands([
173
+ {id: "y", schema: [{id: "cmd", type: "DTP_STRING"}]},
174
+ ]);
175
+ expect(js[0].parameters[0].id).toBe("cmd");
176
+ });
177
+ });
178
+
179
+ describe("normalize_trace_levels", () => {
180
+ it("reads the C dict", () => {
181
+ expect(normalize_trace_levels({info_gclass_trace: {a: "A"}}))
182
+ .toEqual([{id: "a", description: "A"}]);
183
+ });
184
+ it("reads the browser array", () => {
185
+ expect(normalize_trace_levels({trace_levels: [{name: "rows", description: "R"}]}))
186
+ .toEqual([{id: "rows", description: "R"}]);
187
+ });
188
+ it("answers a list when there are none", () => {
189
+ expect(normalize_trace_levels({})).toEqual([]);
190
+ });
191
+ });
192
+
193
+ describe("build_fsm", () => {
194
+ const fsm = build_fsm(C_DIALECT.FSM);
195
+
196
+ it("puts the events in the rows and the states in the columns", () => {
197
+ expect(fsm.states.map((s) => s.name)).toEqual([
198
+ "ST_DISCONNECTED", "ST_WAIT_IDENTITY_CARD_ACK", "ST_SESSION",
199
+ ]);
200
+ expect(fsm.rows.map((r) => r.event)).toEqual([
201
+ "EV_ON_MESSAGE", "EV_ON_OPEN", "EV_ON_CLOSE",
202
+ ]);
203
+ expect(fsm.rows[0].cells.length).toBe(3);
204
+ });
205
+
206
+ it("leaves a cell null where the state declares nothing", () => {
207
+ const on_open = fsm.rows.find((r) => r.event === "EV_ON_OPEN");
208
+ expect(on_open.cells[0].next_state).toBe("ST_WAIT_IDENTITY_CARD_ACK");
209
+ expect(on_open.cells[1]).toBe(null);
210
+ expect(on_open.cells[2]).toBe(null);
211
+ });
212
+
213
+ it("keeps an event out of the matrix when no state handles it", () => {
214
+ expect(fsm.published.map((p) => p.event)).toEqual(["EV_ON_ID_NAK"]);
215
+ expect(fsm.published[0].flags).toEqual(["EVF_OUTPUT_EVENT"]);
216
+ });
217
+
218
+ it("tells a declared-but-actionless cell from an action", () => {
219
+ /* `{EV_TX_READY, 0, 0}` -- four of them in C_WEBSOCKET: the
220
+ * event is legal in that state and deliberately does nothing. */
221
+ const none = build_fsm({
222
+ events: [{event_name: "EV_TX_READY", event_flag: ""}],
223
+ states: {"ST_IDLE": [
224
+ {event_name: "EV_TX_READY", action: "", next_state: ""},
225
+ ]},
226
+ });
227
+ expect(none.rows[0].cells[0]).toEqual({
228
+ action: "", has_action: false, next_state: "",
229
+ });
230
+ });
231
+
232
+ it("says an action exists without inventing its name", () => {
233
+ expect(fsm.rows[0].cells[1]).toEqual({
234
+ action: "", has_action: true, next_state: "",
235
+ });
236
+ });
237
+
238
+ it("keeps the action name when the browser gives one", () => {
239
+ const js = build_fsm(JS_DIALECT.FSM);
240
+ expect(js.rows[0].cells[0].action).toBe("ac_set_json");
241
+ });
242
+
243
+ it("marks a state nothing declares a way into", () => {
244
+ expect(fsm.unreachable).toEqual(["ST_SESSION"]);
245
+ expect(fsm.entry_state).toBe("ST_DISCONNECTED");
246
+ });
247
+
248
+ it("never marks the entry state", () => {
249
+ const one = build_fsm({events: [], states: {"ST_IDLE": []}});
250
+ expect(one.unreachable).toEqual([]);
251
+ });
252
+
253
+ it("counts what each state handles", () => {
254
+ expect(fsm.states.map((s) => s.count)).toEqual([1, 2, 2]);
255
+ });
256
+
257
+ it("shows an event a state handles but the table forgot to declare", () => {
258
+ const orphan = build_fsm({
259
+ events: [],
260
+ states: {"ST_IDLE": [{event_name: "EV_GHOST", action: "ac_x", next_state: ""}]},
261
+ });
262
+ expect(orphan.rows.map((r) => r.event)).toEqual(["EV_GHOST"]);
263
+ expect(orphan.rows[0].flags).toEqual([]);
264
+ });
265
+
266
+ it("survives a gclass with no machine at all", () => {
267
+ const none = build_fsm(undefined);
268
+ expect(none.states).toEqual([]);
269
+ expect(none.rows).toEqual([]);
270
+ expect(none.published).toEqual([]);
271
+ });
272
+ });
273
+
274
+ describe("gclass_view_model", () => {
275
+ it("reads the C dialect whole", () => {
276
+ const m = gclass_view_model(C_DIALECT);
277
+ expect(m.id).toBe("C_IEVENT_CLI");
278
+ expect(m.instances).toBe(4);
279
+ expect(m.priv_size).toBe(312);
280
+ expect(m.attrs[0]).toEqual({
281
+ id: "url", type: "string", flags: ["SDF_PERSIST"],
282
+ default_value: "", description: "Url to connect",
283
+ });
284
+ expect(m.trace_levels.map((r) => r.id)).toEqual(["ievents", "identity-card"]);
285
+ expect(m.commands).toEqual([]);
286
+ });
287
+
288
+ it("reads the browser dialect whole", () => {
289
+ const m = gclass_view_model(JS_DIALECT);
290
+ expect(m.gcflag).toEqual(["gcflag_no_check_output_events"]);
291
+ expect(m.attrs[1].default_value).toBe("null");
292
+ expect(m.commands[0].parameters[0].id).toBe("cmd");
293
+ expect(m.methods).toEqual(["mt_create", "mt_start"]);
294
+ expect(m.instances).toBe(0);
295
+ });
296
+
297
+ it("answers null for nothing", () => {
298
+ expect(gclass_view_model(null)).toBe(null);
299
+ expect(gclass_view_model("C_TIMER")).toBe(null);
300
+ });
301
+
302
+ it("draws empty zones for a description missing keys", () => {
303
+ const m = gclass_view_model({id: "C_BARE"});
304
+ expect(m.attrs).toEqual([]);
305
+ expect(m.commands).toEqual([]);
306
+ expect(m.trace_levels).toEqual([]);
307
+ expect(m.instances).toBe(null);
308
+ expect(m.fsm.rows).toEqual([]);
309
+ });
310
+ });
@@ -6,9 +6,11 @@
6
6
  * The framework had no such viewer. The C kernel answers
7
7
  * `view-gclass` with a full description (attrs, commands,
8
8
  * methods, FSM) and the only reader of it was a terminal.
9
- * This one draws that same document with C_YUI_JSON, so the
10
- * FSM of a gclass is a graph and its attrs table is a tree,
11
- * and it takes the document from either side:
9
+ * This one draws that same document with C_YUI_GCLASS, which
10
+ * lays it out by zones -- attributes, commands, methods,
11
+ * trace levels, and the machine as a matrix -- and keeps the
12
+ * raw answer one button away. It takes the document from
13
+ * either side:
12
14
  *
13
15
  * - a gclass of the browser yuno -> describe_local_gclass()
14
16
  * - a gclass of a backend yuno -> `opts.description`, the
@@ -39,23 +41,38 @@ import {
39
41
  } from "@yuneta/gobj-js";
40
42
 
41
43
  import {describe_local_gclass} from "./gclass_describe.js";
44
+ import {register_c_yui_gclass} from "./c_yui_gclass.js";
42
45
  import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
43
46
  import {yui_shell_of} from "./c_yui_shell.js";
44
47
 
45
48
  import {t} from "i18next";
46
49
 
47
50
 
51
+ /*
52
+ * The gclass that draws a gclass. It is registered on demand here
53
+ * (see gclass_view_available), so a host offering the control does
54
+ * not have to mount it.
55
+ */
56
+ const VIEWER_GCLASS = "C_YUI_GCLASS";
57
+
58
+
48
59
  /************************************************************
49
60
  * Can this app show a gclass at all?
50
61
  *
51
- * The viewer is C_YUI_JSON, and an app registers only the
52
- * gclasses it mounts. The caller asks this BEFORE drawing a
53
- * button, so an app that does not carry the viewer shows no
54
- * control instead of a control that fails.
62
+ * Always: the viewer registers ITSELF here, so no app has to
63
+ * remember to mount a gclass for a control it never asked
64
+ * for. The predicate stays because every caller asks it
65
+ * BEFORE drawing the button, and because registration can
66
+ * still fail -- a gclass_create() that returns -1 leaves the
67
+ * registry without it, and a button that opens nothing is
68
+ * worse than no button.
55
69
  ************************************************************/
56
70
  function gclass_view_available()
57
71
  {
58
- return !!gclass_find_by_name("C_YUI_JSON", false);
72
+ if(!gclass_find_by_name(VIEWER_GCLASS, false)) {
73
+ register_c_yui_gclass();
74
+ }
75
+ return !!gclass_find_by_name(VIEWER_GCLASS, false);
59
76
  }
60
77
 
61
78
 
@@ -65,6 +82,9 @@ function gclass_view_available()
65
82
  * opts:
66
83
  * description the document to draw. Omitted, it is
67
84
  * built from the browser registry.
85
+ * current_state the state the INSTANCE the reader came
86
+ * from is in, lit in the machine. Omitted,
87
+ * nothing is lit.
68
88
  * title_prefix what the window says before the gclass
69
89
  * name (the yuno it belongs to).
70
90
  * on_close called when the reader dismisses it.
@@ -84,7 +104,7 @@ function open_gclass_view(host, gclass_name, opts)
84
104
  let shell = yui_shell_of(host);
85
105
 
86
106
  if(!gclass_view_available()) {
87
- log_error(`${gobj_short_name(host)}: C_YUI_JSON not registered by the app`);
107
+ log_error(`${gobj_short_name(host)}: cannot register ${VIEWER_GCLASS}`);
88
108
  yui_shell_show_error(shell, "gclass viewer unavailable", {t: t});
89
109
  return null;
90
110
  }
@@ -101,13 +121,20 @@ function open_gclass_view(host, gclass_name, opts)
101
121
 
102
122
  let jv = gobj_create_service(
103
123
  `gclass-view-${clean_name(gobj_name(host))}`,
104
- "C_YUI_JSON",
124
+ VIEWER_GCLASS,
105
125
  {
106
- /* No `title`: the host titles it -- the window's title bar
107
- * on desktop, the dialog's header on mobile. The viewer's
108
- * own title would land INSIDE that host, doubling it. */
109
- json_data: description,
110
- subscriber: null,
126
+ /* No title attr: the host titles it -- the window's title
127
+ * bar on desktop, the dialog's header on mobile. The
128
+ * viewer's own title would land INSIDE that host,
129
+ * doubling it. */
130
+ description: description,
131
+ gclass_name: gclass_name,
132
+ /* Which state the INSTANCE the reader came from is in.
133
+ * The description cannot carry it -- it describes the
134
+ * CLASS -- and the caller is the only party that knows,
135
+ * so an omitted one simply lights nothing. */
136
+ current_state: opts.current_state || "",
137
+ subscriber: null,
111
138
  },
112
139
  host
113
140
  );