@yuneta/gobj-ui 7.25.18 → 7.25.20
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 +87 -8
- package/dist/gobj-ui.cjs.js +151 -43
- package/dist/gobj-ui.es.js +151 -43
- package/package.json +2 -2
- package/src/c_g6_nodes_tree.js +52 -4
- package/src/c_yui_json.js +28 -5
- package/src/c_yui_json_pad.js +3 -2
- package/src/c_yui_node.js +71 -0
- package/src/c_yui_schema_editor.js +34 -11
- package/src/c_yui_treedb_graph.js +24 -10
- package/src/c_yui_treedb_topic_with_form.js +24 -0
- package/src/c_yui_treedb_topics.js +22 -9
- package/src/c_yui_uplot.js +10 -5
- package/src/form_json_expand.wiring.test.js +204 -0
- package/src/g6_graphs_write_owed.wiring.test.js +114 -0
- package/src/node_nav_events.wiring.test.js +150 -0
- package/src/schema_editor_reload.wiring.test.js +37 -0
- package/src/schema_editor_transport.wiring.test.js +89 -5
- package/src/treedb_graph_writes.wiring.test.js +38 -4
- package/src/treedb_topics_transport.wiring.test.js +89 -0
- package/src/uplot_add_serie.test.js +92 -0
|
@@ -188,6 +188,43 @@ describe("a refused __graphs__ write keeps the Save lit", () => {
|
|
|
188
188
|
expect(errors()).toEqual([]);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
/* Before gobj-ui 7.25.19 only a Save that planned the topic paid
|
|
192
|
+
* what was owed. Save #1 and Save #2 both write `users`, #1 is
|
|
193
|
+
* refused and its answer lands after #2's echo: the echo made the
|
|
194
|
+
* topic saved, so no Save planned it again, and the button stayed
|
|
195
|
+
* lit over nothing to write until a reload. */
|
|
196
|
+
test("an echo of the topic pays what was owed: the Save goes dark", () => {
|
|
197
|
+
const g = build("edition");
|
|
198
|
+
g.tree.priv.descs = {};
|
|
199
|
+
g.tree.priv.__graphs__ = [];
|
|
200
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
201
|
+
expect(g.lit()).toBe(true);
|
|
202
|
+
|
|
203
|
+
gobj_send_event(g.tree, "EV_NODE_UPDATED", {
|
|
204
|
+
topic_name: "__graphs__",
|
|
205
|
+
node: {id: "users", topic: "users", active: true,
|
|
206
|
+
properties: {nodes: {u1: {x: 30, y: 40}}}}
|
|
207
|
+
}, g.tree);
|
|
208
|
+
expect(g.lit()).toBe(false);
|
|
209
|
+
|
|
210
|
+
gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
|
|
211
|
+
expect(published.filter((p) => p.event === "EV_UPDATE_NODE")).toEqual([]);
|
|
212
|
+
expect(errors()).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("an echo of ANOTHER topic leaves it owed", () => {
|
|
216
|
+
const g = build("edition");
|
|
217
|
+
g.tree.priv.descs = {};
|
|
218
|
+
g.tree.priv.__graphs__ = [];
|
|
219
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
220
|
+
gobj_send_event(g.tree, "EV_NODE_UPDATED", {
|
|
221
|
+
topic_name: "__graphs__",
|
|
222
|
+
node: {id: "roles", topic: "roles", active: true, properties: {nodes: {}}}
|
|
223
|
+
}, g.tree);
|
|
224
|
+
expect(g.lit()).toBe(true);
|
|
225
|
+
expect(errors()).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
|
|
191
228
|
test("a reload forgets it: the view holds what the backend holds", () => {
|
|
192
229
|
const g = build("edition");
|
|
193
230
|
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
@@ -198,3 +235,80 @@ describe("a refused __graphs__ write keeps the Save lit", () => {
|
|
|
198
235
|
expect(errors()).toEqual([]);
|
|
199
236
|
});
|
|
200
237
|
});
|
|
238
|
+
|
|
239
|
+
/* A refusal is about the LOAD its write was sent from. Before gobj-ui
|
|
240
|
+
* 7.25.20 it carried only the topic: Save, then Refresh, then the
|
|
241
|
+
* refusal landed and was applied to the FRESH load -- the Save lit
|
|
242
|
+
* over nothing changed, and pressing it wrote a record identical to
|
|
243
|
+
* what the backend holds. */
|
|
244
|
+
describe("a refusal of an earlier load is not applied to the current one", () => {
|
|
245
|
+
|
|
246
|
+
test("the write carries its load, and a refusal of it counts", () => {
|
|
247
|
+
const g = build("edition");
|
|
248
|
+
g.tree.priv._graph_properties.users.nodes.u1.x = 99;
|
|
249
|
+
gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
|
|
250
|
+
const [write] = published.filter((p) => p.event === "EV_UPDATE_NODE");
|
|
251
|
+
expect(typeof write.kw.graphs_load).toBe("number");
|
|
252
|
+
|
|
253
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED",
|
|
254
|
+
{topic: "users", graphs_load: write.kw.graphs_load}, g.tree);
|
|
255
|
+
expect(g.lit()).toBe(true);
|
|
256
|
+
expect(g.tree.priv._saved_graph_properties.users).toBe(undefined);
|
|
257
|
+
expect(errors()).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("Save, reload, then the refusal: ignored, and said", () => {
|
|
261
|
+
const g = build("edition");
|
|
262
|
+
g.tree.priv._graph_properties.users.nodes.u1.x = 99;
|
|
263
|
+
gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
|
|
264
|
+
const [write] = published.filter((p) => p.event === "EV_UPDATE_NODE");
|
|
265
|
+
|
|
266
|
+
gobj_send_event(g.tree, "EV_CLEAR_DATA", {}, g.tree);
|
|
267
|
+
/* The reload brings what the backend holds. */
|
|
268
|
+
g.tree.priv._graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
|
|
269
|
+
g.tree.priv._saved_graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
|
|
270
|
+
gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
|
|
271
|
+
logged.length = 0;
|
|
272
|
+
|
|
273
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED",
|
|
274
|
+
{topic: "users", graphs_load: write.kw.graphs_load}, g.tree);
|
|
275
|
+
expect(g.lit()).toBe(false);
|
|
276
|
+
expect(g.tree.priv._saved_graph_properties.users).toEqual({nodes: {u1: {x: 10, y: 20}}});
|
|
277
|
+
expect(logged.map((l) => [l.level, l.msg.replace(/^\S+: /, "")])).toEqual([
|
|
278
|
+
["warning", "a refused write of the arrangement of 'users' belongs to an earlier load: ignored"]
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/* The FIRST record of a topic's arrangement arrives as a CREATE echo
|
|
284
|
+
* (from this browser or another). Before gobj-ui 7.25.20 it rebuilt
|
|
285
|
+
* every topic's saved copy from the live objects the view edits in
|
|
286
|
+
* place, so an unsaved change of ANOTHER topic counted as saved and
|
|
287
|
+
* the next Save did not write it. */
|
|
288
|
+
describe("a __graphs__ create echo moves only its own topic", () => {
|
|
289
|
+
|
|
290
|
+
test("an unsaved change of another topic stays unsaved", () => {
|
|
291
|
+
const g = build("edition");
|
|
292
|
+
g.tree.priv.descs = {};
|
|
293
|
+
g.tree.priv.__graphs__ = [
|
|
294
|
+
{id: "users", topic: "users", active: true,
|
|
295
|
+
properties: g.tree.priv._graph_properties.users}
|
|
296
|
+
];
|
|
297
|
+
/* Edited in place, not saved. */
|
|
298
|
+
g.tree.priv._graph_properties.users.nodes.u1.x = 77;
|
|
299
|
+
|
|
300
|
+
gobj_send_event(g.tree, "EV_NODE_CREATED", {
|
|
301
|
+
topic_name: "__graphs__",
|
|
302
|
+
node: {id: "roles", topic: "roles", active: true,
|
|
303
|
+
properties: {nodes: {r1: {x: 1, y: 2}}}}
|
|
304
|
+
}, g.tree);
|
|
305
|
+
|
|
306
|
+
expect(g.tree.priv._saved_graph_properties.users).toEqual({nodes: {u1: {x: 10, y: 20}}});
|
|
307
|
+
expect(g.tree.priv._saved_graph_properties.roles).toEqual({nodes: {r1: {x: 1, y: 2}}});
|
|
308
|
+
|
|
309
|
+
gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
|
|
310
|
+
const writes = published.filter((p) => p.event === "EV_UPDATE_NODE");
|
|
311
|
+
expect(writes.map((w) => w.kw.record.id)).toEqual(["users"]);
|
|
312
|
+
expect(errors()).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* node_nav_events.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* C_YUI_NODE hosts C_YUI_NAV as a pure CHILD, so it hears every
|
|
5
|
+
* event the nav publishes -- and it declared only EV_NAV_CLICKED.
|
|
6
|
+
* EV_NAV_ITEM_CLOSE (the ✕ of a closable item) and
|
|
7
|
+
* EV_DRAWER_CLOSE_REQUESTED (the backdrop of a drawer projection,
|
|
8
|
+
* a layout a node may declare) answered "Event NOT DEFINED in
|
|
9
|
+
* state" -- and the drawer stayed open, because the shell that
|
|
10
|
+
* closes drawers knows only its own navs. A zone projection also
|
|
11
|
+
* outlives the path (it is stopped only in mt_stop), so the clicks
|
|
12
|
+
* of a root that is off the path landed on ST_OFF, which declared
|
|
13
|
+
* none of the three.
|
|
14
|
+
*
|
|
15
|
+
* Driven through the real gclasses (C_YUI_SHELL, C_YUI_NODE,
|
|
16
|
+
* C_YUI_NAV) on a document double: each event is published by the
|
|
17
|
+
* real nav the node created.
|
|
18
|
+
*
|
|
19
|
+
* Copyright (c) 2026, ArtGins.
|
|
20
|
+
* All Rights Reserved.
|
|
21
|
+
***********************************************************************/
|
|
22
|
+
import {describe, test, expect, beforeAll, beforeEach} from "vitest";
|
|
23
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
24
|
+
|
|
25
|
+
install_dom_double();
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
SDATA_END, gclass_create,
|
|
29
|
+
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_pure_child,
|
|
30
|
+
gobj_start, gobj_send_event, gobj_publish_event, gobj_read_attr,
|
|
31
|
+
gobj_current_state, gobj_change_state,
|
|
32
|
+
set_log_callback,
|
|
33
|
+
} = await import("@yuneta/gobj-js");
|
|
34
|
+
const {register_c_yui_shell} = await import("./c_yui_shell.js");
|
|
35
|
+
const {register_c_yui_nav} = await import("./c_yui_nav.js");
|
|
36
|
+
const {register_c_yui_node} = await import("./c_yui_node.js");
|
|
37
|
+
|
|
38
|
+
const logged = [];
|
|
39
|
+
const closed = []; /* the EV_NAV_ITEM_CLOSE the app heard */
|
|
40
|
+
let yuno = null;
|
|
41
|
+
|
|
42
|
+
beforeAll(() => {
|
|
43
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
44
|
+
set_log_callback((level, msg) => {
|
|
45
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
46
|
+
});
|
|
47
|
+
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
48
|
+
/* An app that uses closable items: it declares what the shell
|
|
49
|
+
* re-publishes. */
|
|
50
|
+
gclass_create(
|
|
51
|
+
"C_TEST_APP",
|
|
52
|
+
[["EV_NAV_ITEM_CLOSE", 0]],
|
|
53
|
+
[["ST_IDLE", [
|
|
54
|
+
["EV_NAV_ITEM_CLOSE", (gobj, event, kw) => {
|
|
55
|
+
closed.push(kw);
|
|
56
|
+
return 0;
|
|
57
|
+
}, null]
|
|
58
|
+
]]],
|
|
59
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
60
|
+
);
|
|
61
|
+
register_c_yui_shell();
|
|
62
|
+
register_c_yui_nav();
|
|
63
|
+
register_c_yui_node();
|
|
64
|
+
yuno = gobj_create_yuno("node_nav_yuno", "C_TEST_HOST", {});
|
|
65
|
+
gobj_start(yuno);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
logged.length = 0;
|
|
70
|
+
closed.length = 0;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let seq = 0;
|
|
74
|
+
|
|
75
|
+
/* A tree under a shell, on the path, projecting its children with
|
|
76
|
+
* `layout`. Returns the tree and the nav it created. */
|
|
77
|
+
function build(layout)
|
|
78
|
+
{
|
|
79
|
+
const name = `n${++seq}`;
|
|
80
|
+
const app = gobj_create(`${name}_app`, "C_TEST_APP", {}, yuno);
|
|
81
|
+
gobj_start(app);
|
|
82
|
+
const shell = gobj_create(`${name}_shell`, "C_YUI_SHELL",
|
|
83
|
+
{config: {shell: {}}, default_route: "/home", subscriber: app}, app);
|
|
84
|
+
gobj_start(shell);
|
|
85
|
+
const tree = gobj_create_pure_child(`${name}_tree`, "C_YUI_NODE", {
|
|
86
|
+
node_id: "root",
|
|
87
|
+
base_route: "/tree",
|
|
88
|
+
projection: {index: {layout: layout}},
|
|
89
|
+
children: [{id: "a", label: "A"}, {id: "b", label: "B"}]
|
|
90
|
+
}, shell);
|
|
91
|
+
gobj_start(tree);
|
|
92
|
+
gobj_send_event(tree, "EV_ROUTE_CHANGED", {base: "/tree", subpath: ""}, shell);
|
|
93
|
+
expect(gobj_current_state(tree)).toBe("ST_SELF");
|
|
94
|
+
const nav = tree.priv.navs[0];
|
|
95
|
+
expect(nav).toBeTruthy();
|
|
96
|
+
expect(gobj_read_attr(nav, "layout")).toBe(layout);
|
|
97
|
+
logged.length = 0;
|
|
98
|
+
return {app, shell, tree, nav};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function errors()
|
|
102
|
+
{
|
|
103
|
+
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("the events of a C_YUI_NAV reach its C_YUI_NODE", () => {
|
|
107
|
+
|
|
108
|
+
test("EV_NAV_ITEM_CLOSE is handed to the shell, and the app hears it", () => {
|
|
109
|
+
const {nav} = build("tabs");
|
|
110
|
+
gobj_publish_event(nav, "EV_NAV_ITEM_CLOSE", {
|
|
111
|
+
item_id: "a", route: "/tree/a", menu_id: "node./tree", zone: ""
|
|
112
|
+
});
|
|
113
|
+
expect(errors()).toEqual([]);
|
|
114
|
+
expect(closed).toEqual([{
|
|
115
|
+
item_id: "a", route: "/tree/a", menu_id: "node./tree", zone: ""
|
|
116
|
+
}]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("EV_DRAWER_CLOSE_REQUESTED closes the node's own drawer", () => {
|
|
120
|
+
const {nav} = build("drawer");
|
|
121
|
+
const $drawer = gobj_read_attr(nav, "$container");
|
|
122
|
+
$drawer.classList.add("is-active");
|
|
123
|
+
|
|
124
|
+
gobj_publish_event(nav, "EV_DRAWER_CLOSE_REQUESTED", {
|
|
125
|
+
menu_id: gobj_read_attr(nav, "menu_id")
|
|
126
|
+
});
|
|
127
|
+
expect(errors()).toEqual([]);
|
|
128
|
+
expect($drawer.classList.contains("is-active")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a drawer of another menu is left open", () => {
|
|
132
|
+
const {nav} = build("drawer");
|
|
133
|
+
const $drawer = gobj_read_attr(nav, "$container");
|
|
134
|
+
$drawer.classList.add("is-active");
|
|
135
|
+
gobj_publish_event(nav, "EV_DRAWER_CLOSE_REQUESTED", {menu_id: "other"});
|
|
136
|
+
expect(errors()).toEqual([]);
|
|
137
|
+
expect($drawer.classList.contains("is-active")).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("off the path, a nav that is still there is heard too", () => {
|
|
141
|
+
const {tree, nav} = build("tabs");
|
|
142
|
+
/* What a zone nav is: alive while its root is off the path. */
|
|
143
|
+
gobj_change_state(tree, "ST_OFF");
|
|
144
|
+
gobj_publish_event(nav, "EV_NAV_ITEM_CLOSE", {item_id: "b"});
|
|
145
|
+
gobj_publish_event(nav, "EV_DRAWER_CLOSE_REQUESTED", {menu_id: "x"});
|
|
146
|
+
gobj_publish_event(nav, "EV_NAV_CLICKED", {route: "/tree/a", item_id: "a"});
|
|
147
|
+
expect(errors()).toEqual([]);
|
|
148
|
+
expect(closed.map((c) => c.item_id)).toEqual(["b"]);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -481,6 +481,43 @@ describe("the drawing of a treedb that is not there", () => {
|
|
|
481
481
|
});
|
|
482
482
|
});
|
|
483
483
|
|
|
484
|
+
/*
|
|
485
|
+
* Since gobj-ui 7.25.17 the notice translated its detail, and three
|
|
486
|
+
* callers pass a NAME there: a topic called `nodes` that is gone was
|
|
487
|
+
* shown as "Nodos" in an app whose locale has that key, and followed
|
|
488
|
+
* every change of language.
|
|
489
|
+
*/
|
|
490
|
+
describe("the name in a notice is a name", () => {
|
|
491
|
+
|
|
492
|
+
test("a topic that is gone is named as it is, with no i18n key", async () => {
|
|
493
|
+
const i18next = (await import("i18next")).default;
|
|
494
|
+
await i18next.init({lng: "es", resources: {es: {translation: {nodes: "Nodos", db: "BD"}}}});
|
|
495
|
+
try {
|
|
496
|
+
const {editor} = build("nn1", "db/nodes");
|
|
497
|
+
expect($in(editor, ".SCHEMA_NOTICE_TEXT").getAttribute("data-i18n"))
|
|
498
|
+
.toBe("that topic is not here any more");
|
|
499
|
+
const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
|
|
500
|
+
expect($detail.textContent).toBe("nodes");
|
|
501
|
+
expect($detail.getAttribute("data-i18n")).toBe(null);
|
|
502
|
+
} finally {
|
|
503
|
+
await i18next.init({lng: "en", resources: {}});
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("a treedb that is gone is named as it is, with no i18n key", async () => {
|
|
508
|
+
const i18next = (await import("i18next")).default;
|
|
509
|
+
await i18next.init({lng: "es", resources: {es: {translation: {nodes: "Nodos"}}}});
|
|
510
|
+
try {
|
|
511
|
+
const {editor} = build("nn2", "nodes");
|
|
512
|
+
const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
|
|
513
|
+
expect($detail.textContent).toBe("nodes");
|
|
514
|
+
expect($detail.getAttribute("data-i18n")).toBe(null);
|
|
515
|
+
} finally {
|
|
516
|
+
await i18next.init({lng: "en", resources: {}});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
484
521
|
describe("a write given up that was DONE", () => {
|
|
485
522
|
|
|
486
523
|
test("the host is told, and the reload keeps what this session wrote", () => {
|
|
@@ -37,7 +37,7 @@ const {
|
|
|
37
37
|
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
|
|
38
38
|
gobj_start, gobj_send_event, gobj_read_attr,
|
|
39
39
|
gobj_change_state, gobj_current_state,
|
|
40
|
-
set_log_callback,
|
|
40
|
+
set_log_callback, log_error,
|
|
41
41
|
} = await import("@yuneta/gobj-js");
|
|
42
42
|
const {register_c_yui_schema_editor} = await import("./c_yui_schema_editor.js");
|
|
43
43
|
|
|
@@ -56,13 +56,33 @@ const RECORDS = {
|
|
|
56
56
|
],
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
/* A transport out of session refuses in the RETURN, as
|
|
60
|
-
*
|
|
59
|
+
/* A transport out of session refuses in the RETURN, as the routing
|
|
60
|
+
* adapters do (gui_agent's C_AGENT_TREEDB_LINK): a request that did not
|
|
61
|
+
* leave is not waited for. `refuse_on_the_way` makes it refuse IN
|
|
62
|
+
* session too, as a transport that cannot send. */
|
|
63
|
+
let refuse_on_the_way = false;
|
|
64
|
+
|
|
61
65
|
function remote_command_parser(gobj, command, kw, src)
|
|
62
66
|
{
|
|
63
67
|
if(gobj_current_state(gobj) !== "ST_SESSION") {
|
|
64
68
|
return `${command}: not in session`;
|
|
65
69
|
}
|
|
70
|
+
if(refuse_on_the_way) {
|
|
71
|
+
return `${command}: cannot send`;
|
|
72
|
+
}
|
|
73
|
+
commands.push({command, kw, src});
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* A DIRECT C_IEVENT_CLI, as its mt_command does it: out of session it
|
|
78
|
+
* logs "Not in session" and answers NULL -- the same answer as a request
|
|
79
|
+
* that left. The README allows `gobj_remote_yuno` to be one. */
|
|
80
|
+
function ievent_cli_command_parser(gobj, command, kw, src)
|
|
81
|
+
{
|
|
82
|
+
if(gobj_current_state(gobj) !== "ST_SESSION") {
|
|
83
|
+
log_error(`Not in session`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
66
86
|
commands.push({command, kw, src});
|
|
67
87
|
return null;
|
|
68
88
|
}
|
|
@@ -93,6 +113,13 @@ beforeAll(() => {
|
|
|
93
113
|
{mt_command_parser: remote_command_parser},
|
|
94
114
|
0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
95
115
|
);
|
|
116
|
+
gclass_create(
|
|
117
|
+
"C_TEST_IEVENT_CLI",
|
|
118
|
+
[],
|
|
119
|
+
[["ST_DISCONNECTED", []], ["ST_SESSION", []]],
|
|
120
|
+
{mt_command_parser: ievent_cli_command_parser},
|
|
121
|
+
0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
122
|
+
);
|
|
96
123
|
register_c_yui_schema_editor();
|
|
97
124
|
yuno = gobj_create_yuno("transport_yuno", "C_TEST_HOST", {});
|
|
98
125
|
gobj_start(yuno);
|
|
@@ -102,6 +129,7 @@ beforeEach(() => {
|
|
|
102
129
|
logged.length = 0;
|
|
103
130
|
commands.length = 0;
|
|
104
131
|
shown.length = 0;
|
|
132
|
+
refuse_on_the_way = false;
|
|
105
133
|
});
|
|
106
134
|
|
|
107
135
|
function take(command)
|
|
@@ -148,9 +176,10 @@ function reconnect(editor, remote, host)
|
|
|
148
176
|
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: true}, host);
|
|
149
177
|
}
|
|
150
178
|
|
|
151
|
-
function build(name, loaded)
|
|
179
|
+
function build(name, loaded, remote_gclass)
|
|
152
180
|
{
|
|
153
|
-
const remote = gobj_create_service(`${name}_remote`,
|
|
181
|
+
const remote = gobj_create_service(`${name}_remote`,
|
|
182
|
+
remote_gclass || "C_TEST_REMOTE", {}, yuno);
|
|
154
183
|
gobj_change_state(remote, "ST_SESSION");
|
|
155
184
|
const host = gobj_create(`${name}_host`, "C_TEST_EDITOR_HOST", {}, yuno);
|
|
156
185
|
const editor = gobj_create(`${name}_editor`, "C_YUI_SCHEMA_EDITOR", {
|
|
@@ -455,3 +484,58 @@ describe("a write answered after it was given up", () => {
|
|
|
455
484
|
expect(errors()).toEqual([]);
|
|
456
485
|
});
|
|
457
486
|
});
|
|
487
|
+
|
|
488
|
+
/* Before gobj-ui 7.25.20 a write never asked whether the transport was
|
|
489
|
+
* in session. A direct C_IEVENT_CLI answers null out of session, which
|
|
490
|
+
* remote_command() took for "sent": the editor sat in ST_SAVING, busy
|
|
491
|
+
* and inert, until another drop. */
|
|
492
|
+
describe("a write with the transport out of session", () => {
|
|
493
|
+
|
|
494
|
+
test("a direct C_IEVENT_CLI: the write ends, said, and the next session reads again", () => {
|
|
495
|
+
const {editor, remote, host} = build("x1", true, "C_TEST_IEVENT_CLI");
|
|
496
|
+
/* Gone, and the host's edge has not arrived yet. */
|
|
497
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
498
|
+
|
|
499
|
+
gobj_send_event(editor, "EV_SAVE_COLUMN", {
|
|
500
|
+
creating: true, topic: "users", values: {value: "name", type: "string"}
|
|
501
|
+
}, host);
|
|
502
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
503
|
+
expect(is_busy(editor)).toBe(false);
|
|
504
|
+
expect(take("update-node")).toEqual([]);
|
|
505
|
+
expect(shown).toEqual(["cannot reach the treedb"]);
|
|
506
|
+
expect(errors()).toEqual([]);
|
|
507
|
+
expect(warnings()).toEqual([
|
|
508
|
+
"C_YUI_SCHEMA_EDITOR^x1_editor: no session, 'update-node' of 'cols' was not sent"
|
|
509
|
+
]);
|
|
510
|
+
|
|
511
|
+
gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
|
|
512
|
+
reconnect(editor, remote, host);
|
|
513
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
514
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
515
|
+
expect(errors()).toEqual([]);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
/* The toast is handed the KEY (yui_shell_show_error translates it
|
|
519
|
+
* and keeps it for a change of language). This path passed
|
|
520
|
+
* t("cannot reach the treedb"), a text that cannot re-translate. */
|
|
521
|
+
test("a transport that refuses in session: the toast gets the key", async () => {
|
|
522
|
+
const i18next = (await import("i18next")).default;
|
|
523
|
+
if(!i18next.isInitialized) {
|
|
524
|
+
await i18next.init({lng: "en", resources: {}});
|
|
525
|
+
}
|
|
526
|
+
i18next.addResource(i18next.language || "en", "translation",
|
|
527
|
+
"cannot reach the treedb", "TRANSLATED: cannot reach the treedb");
|
|
528
|
+
try {
|
|
529
|
+
const {editor, remote, host} = build("x2", true);
|
|
530
|
+
refuse_on_the_way = true;
|
|
531
|
+
gobj_send_event(editor, "EV_SAVE_COLUMN", {
|
|
532
|
+
creating: true, topic: "users", values: {value: "name", type: "string"}
|
|
533
|
+
}, host);
|
|
534
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
535
|
+
expect(shown).toEqual(["cannot reach the treedb"]);
|
|
536
|
+
expect(errors()).toEqual(["update-node: cannot send"]);
|
|
537
|
+
} finally {
|
|
538
|
+
i18next.removeResourceBundle(i18next.language || "en", "translation");
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
});
|
|
@@ -156,15 +156,20 @@ function build(name)
|
|
|
156
156
|
return {remote, host, engine};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
/* What the engine publishes on a Save of one topic's arrangement
|
|
160
|
-
|
|
159
|
+
/* What the engine publishes on a Save of one topic's arrangement,
|
|
160
|
+
* from its load number `graphs_load`. */
|
|
161
|
+
function save_arrangement(engine, topic, graphs_load)
|
|
161
162
|
{
|
|
162
|
-
|
|
163
|
+
let kw = {
|
|
163
164
|
treedb_name: "treedb_test",
|
|
164
165
|
topic_name: "__graphs__",
|
|
165
166
|
record: {id: topic, topic: topic, active: true, properties: {nodes: {}}},
|
|
166
167
|
options: {list_dict: true, autolink: false, create: true}
|
|
167
|
-
}
|
|
168
|
+
};
|
|
169
|
+
if(graphs_load !== undefined) {
|
|
170
|
+
kw.graphs_load = graphs_load;
|
|
171
|
+
}
|
|
172
|
+
gobj_publish_event(engine, "EV_UPDATE_NODE", kw);
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
/* The answer as the real transports build it: the command frame
|
|
@@ -188,6 +193,11 @@ function refused()
|
|
|
188
193
|
return told.filter((t) => t.event === "EV_GRAPHS_WRITE_REFUSED").map((t) => t.kw.topic);
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
function refused_kw()
|
|
197
|
+
{
|
|
198
|
+
return told.filter((t) => t.event === "EV_GRAPHS_WRITE_REFUSED").map((t) => t.kw);
|
|
199
|
+
}
|
|
200
|
+
|
|
191
201
|
describe("a __graphs__ write that does not land is said to the engine", () => {
|
|
192
202
|
|
|
193
203
|
test("refused by the backend", () => {
|
|
@@ -222,6 +232,30 @@ describe("a __graphs__ write that does not land is said to the engine", () => {
|
|
|
222
232
|
expect(refused()).toEqual(["binaries"]);
|
|
223
233
|
});
|
|
224
234
|
|
|
235
|
+
/* Before gobj-ui 7.25.20 the refusal carried only the topic, and
|
|
236
|
+
* the engine applied one answered after a reload to the fresh
|
|
237
|
+
* load. The engine's load number now travels in __md_command__
|
|
238
|
+
* and comes back with the refusal, on every path. */
|
|
239
|
+
test("the engine's load comes back with the refusal, on every path", () => {
|
|
240
|
+
const {remote, host, engine} = build("g6");
|
|
241
|
+
save_arrangement(engine, "yunos", 7);
|
|
242
|
+
expect(commands[0].kw.__md_command__.graphs_load).toBe(7);
|
|
243
|
+
answer(host, remote, commands[0], -1, "not authorized");
|
|
244
|
+
|
|
245
|
+
refuse_on_the_way = true;
|
|
246
|
+
save_arrangement(engine, "binaries", 7);
|
|
247
|
+
refuse_on_the_way = false;
|
|
248
|
+
|
|
249
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
250
|
+
save_arrangement(engine, "realms", 8);
|
|
251
|
+
|
|
252
|
+
expect(refused_kw()).toEqual([
|
|
253
|
+
{topic: "yunos", graphs_load: 7},
|
|
254
|
+
{topic: "binaries", graphs_load: 7},
|
|
255
|
+
{topic: "realms", graphs_load: 8}
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
225
259
|
test("a refused write of another topic says nothing to the engine", () => {
|
|
226
260
|
const {remote, host, engine} = build("g5");
|
|
227
261
|
gobj_send_event(host, "EV_UPDATE_NODE", {
|
|
@@ -262,6 +262,95 @@ describe("a Save in flight when the backend drops", () => {
|
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
+
/*
|
|
266
|
+
* The success answer of a request, the way C_IEVENT_CLI delivers it: the
|
|
267
|
+
* request's `__md_command__` as the frame, and nothing else of it.
|
|
268
|
+
*/
|
|
269
|
+
function answer_ok(request, data)
|
|
270
|
+
{
|
|
271
|
+
return {result: 0, comment: "", data: data, __md_iev__: {
|
|
272
|
+
command_stack: [{command: request.command, kw: request.kw.__md_command__}]
|
|
273
|
+
}};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/*
|
|
277
|
+
* Before gobj-ui 7.25.19 a delete answered published nothing (the case
|
|
278
|
+
* was an empty `break`), and `created` compared the command with
|
|
279
|
+
* "create-node" -- which the view never sends: its +New goes out as an
|
|
280
|
+
* update-node with `create_only`, so `created` was always false.
|
|
281
|
+
*/
|
|
282
|
+
describe("EV_RECORD_WRITTEN of a create and of a delete", () => {
|
|
283
|
+
|
|
284
|
+
test("a +New answered says created: true", () => {
|
|
285
|
+
const {shell, topics, forms} = build("tc1");
|
|
286
|
+
yui_shell_set_connection_state(shell, true);
|
|
287
|
+
gobj_send_event(topics, "EV_CREATE_RECORD",
|
|
288
|
+
{record: {id: "n1"}, form_write: 1}, forms.users);
|
|
289
|
+
|
|
290
|
+
const request = commands.find((c) => c.command === "update-node");
|
|
291
|
+
expect(request.kw.options.create_only).toBe(true);
|
|
292
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER",
|
|
293
|
+
answer_ok(request, [{id: "n1", name: "new"}]), remote);
|
|
294
|
+
|
|
295
|
+
expect(written).toEqual([{
|
|
296
|
+
treedb_name: "treedb_test",
|
|
297
|
+
topic_name: "users",
|
|
298
|
+
record: {id: "n1", name: "new"},
|
|
299
|
+
created: true,
|
|
300
|
+
command: "update-node"
|
|
301
|
+
}]);
|
|
302
|
+
expect(answers).toEqual([
|
|
303
|
+
{topic: "users", event: "EV_WRITE_DONE", form_write: 1},
|
|
304
|
+
]);
|
|
305
|
+
expect(errors()).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("an edit answered says created: false", () => {
|
|
309
|
+
const {shell, topics, forms} = build("tc2");
|
|
310
|
+
yui_shell_set_connection_state(shell, true);
|
|
311
|
+
save(topics, forms.users, 1);
|
|
312
|
+
const request = commands.find((c) => c.command === "update-node");
|
|
313
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER",
|
|
314
|
+
answer_ok(request, [{id: "x"}]), remote);
|
|
315
|
+
expect(written.map((w) => w.created)).toEqual([false]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("a delete answered is published with the node deleted", () => {
|
|
319
|
+
const {shell, topics, forms} = build("tc3");
|
|
320
|
+
yui_shell_set_connection_state(shell, true);
|
|
321
|
+
gobj_send_event(topics, "EV_DELETE_RECORD", {record: {id: "x"}}, forms.roles);
|
|
322
|
+
|
|
323
|
+
const request = commands.find((c) => c.command === "delete-node");
|
|
324
|
+
expect(request).toBeTruthy();
|
|
325
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER",
|
|
326
|
+
answer_ok(request, [{id: "x", name: "gone"}]), remote);
|
|
327
|
+
|
|
328
|
+
expect(written).toEqual([{
|
|
329
|
+
treedb_name: "treedb_test",
|
|
330
|
+
topic_name: "roles",
|
|
331
|
+
record: {id: "x", name: "gone"},
|
|
332
|
+
created: false,
|
|
333
|
+
command: "delete-node"
|
|
334
|
+
}]);
|
|
335
|
+
/* A delete has no form waiting for its answer. */
|
|
336
|
+
expect(answers).toEqual([]);
|
|
337
|
+
expect(errors()).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("a delete refused publishes nothing", () => {
|
|
341
|
+
const {shell, topics, forms} = build("tc4");
|
|
342
|
+
yui_shell_set_connection_state(shell, true);
|
|
343
|
+
gobj_send_event(topics, "EV_DELETE_RECORD", {record: {id: "x"}}, forms.roles);
|
|
344
|
+
const request = commands.find((c) => c.command === "delete-node");
|
|
345
|
+
gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", {
|
|
346
|
+
result: -1, comment: "linked", data: null, __md_iev__: {
|
|
347
|
+
command_stack: [{command: "delete-node", kw: request.kw.__md_command__}]
|
|
348
|
+
}
|
|
349
|
+
}, remote);
|
|
350
|
+
expect(written).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
265
354
|
/*
|
|
266
355
|
* The answer the routing adapter gives a write it had in flight when
|
|
267
356
|
* its session closed: a failure, settled by the adapter itself.
|