@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.
@@ -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 C_IEVENT_CLI and
60
- * the routing adapters do: a request that did not leave is not waited for. */
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`, "C_TEST_REMOTE", {}, yuno);
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
- function save_arrangement(engine, topic)
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
- gobj_publish_event(engine, "EV_UPDATE_NODE", {
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.