@yuneta/gobj-ui 7.25.15 → 7.25.17

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.
@@ -1520,6 +1520,12 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1520
1520
  kw.__md_command__ = { // Data to be returned
1521
1521
  topic_name: topic_name,
1522
1522
  };
1523
+ /* The answer carries this echo and nothing else of the request: a
1524
+ * refused `__graphs__` write is told to the engine by the topic it
1525
+ * arranges, so that topic has to travel here. */
1526
+ if(topic_name === "__graphs__") {
1527
+ kw.__md_command__.graph_topic = graph_topic_of(record);
1528
+ }
1523
1529
 
1524
1530
  let ret = gobj_command(priv.gobj_remote_yuno,
1525
1531
  command,
@@ -2118,7 +2124,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2118
2124
  if(result < 0 && command === "update-node") {
2119
2125
  graphs_write_refused(gobj,
2120
2126
  kw_get_str(gobj, kw_command, "topic_name", "", 0),
2121
- kw_get_dict(gobj, kw_command, "record", {}, 0));
2127
+ kw_get_str(gobj, kw_command, "graph_topic", "", 0));
2122
2128
  }
2123
2129
  if(result >= 0 &&
2124
2130
  kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
@@ -2486,7 +2492,7 @@ function ac_update_node(gobj, event, kw, src)
2486
2492
  let options = kw.options || {};
2487
2493
 
2488
2494
  if(refuse_if_readonly(gobj, event)) {
2489
- graphs_write_refused(gobj, topic_name, record);
2495
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2490
2496
  return -1;
2491
2497
  }
2492
2498
 
@@ -2496,7 +2502,7 @@ function ac_update_node(gobj, event, kw, src)
2496
2502
  if(topic_name === "__graphs__" && !is_connected(gobj)) {
2497
2503
  log_warning(`${gobj_short_name(gobj)}: no session, the arrangement ` +
2498
2504
  `was not sent`);
2499
- graphs_write_refused(gobj, topic_name, record);
2505
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2500
2506
  return -1;
2501
2507
  }
2502
2508
 
@@ -2508,11 +2514,22 @@ function ac_update_node(gobj, event, kw, src)
2508
2514
  options
2509
2515
  );
2510
2516
  if(ret < 0) {
2511
- graphs_write_refused(gobj, topic_name, record);
2517
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2512
2518
  }
2513
2519
  return ret;
2514
2520
  }
2515
2521
 
2522
+ /************************************************************
2523
+ * The topic a `__graphs__` record arranges.
2524
+ ************************************************************/
2525
+ function graph_topic_of(record)
2526
+ {
2527
+ if(!record || typeof record !== "object") {
2528
+ return "";
2529
+ }
2530
+ return record.topic || record.id || "";
2531
+ }
2532
+
2516
2533
  /************************************************************
2517
2534
  * A write of `__graphs__` did not land. The engine
2518
2535
  * (C_G6_NODES_TREE) took it for granted when it left -- nothing
@@ -2520,14 +2537,13 @@ function ac_update_node(gobj, event, kw, src)
2520
2537
  * writes the topic again. Other topics are the operator's data:
2521
2538
  * the answer's error says what happened to them.
2522
2539
  ************************************************************/
2523
- function graphs_write_refused(gobj, topic_name, record)
2540
+ function graphs_write_refused(gobj, topic_name, topic)
2524
2541
  {
2525
2542
  let priv = gobj.priv;
2526
2543
 
2527
2544
  if(topic_name !== "__graphs__") {
2528
2545
  return;
2529
2546
  }
2530
- let topic = (record && typeof record === "object") ? (record.topic || record.id) : "";
2531
2547
  if(!topic) {
2532
2548
  log_error(`${gobj_short_name(gobj)}: a refused __graphs__ write names no topic`);
2533
2549
  return;
@@ -53,7 +53,7 @@ describe("set_toolbar_busy", () => {
53
53
  expect(tb.save.classList.contains("is-loading")).toBe(false);
54
54
  });
55
55
 
56
- it("busy twice then free: Save and Cancel come back (N9)", () => {
56
+ it("busy twice then free: Save and Cancel come back", () => {
57
57
  /* A record with a picked file went busy for the read and busy
58
58
  * again for the write; the second pass recorded the buttons the
59
59
  * first had disabled as disabled on their own, and the free of a
@@ -30,7 +30,7 @@ describe("form writes in flight", () => {
30
30
  ]);
31
31
  });
32
32
 
33
- it("the transport closes: every write in flight is abandoned, once (N8)", () => {
33
+ it("the transport closes: every write in flight is abandoned, once", () => {
34
34
  /* The form of each one is answered EV_WRITE_REFUSED by the host
35
35
  * and stays open on what was typed; before, it stayed busy for
36
36
  * ever, because nobody answered a write whose answer was lost. */
@@ -0,0 +1,200 @@
1
+ /***********************************************************************
2
+ * g6_graphs_write_owed.wiring.test.js
3
+ *
4
+ * A `__graphs__` write that did not land is OWED, and the Save
5
+ * button says so until a Save writes it again.
6
+ *
7
+ * EV_GRAPHS_WRITE_REFUSED lit the Save (in edition only), and
8
+ * the next repaint of the history buttons put it out again:
9
+ * update_history_buttons() decides the Save from
10
+ * `history.canUndo()` alone, and it runs on every history
11
+ * change, every change of mode and every redraw of the theme.
12
+ * A refusal heard in reading was never shown at all.
13
+ *
14
+ * The real C_G6_NODES_TREE, on a fake G6 graph: only the history
15
+ * plugin and the Save button are what is looked at.
16
+ *
17
+ * Copyright (c) 2026, ArtGins.
18
+ * All Rights Reserved.
19
+ ***********************************************************************/
20
+ import {describe, test, expect, beforeAll, beforeEach} from "vitest";
21
+ import {install_dom_double} from "../test/dom_double.js";
22
+
23
+ install_dom_double();
24
+
25
+ const {
26
+ SDATA, SDATA_END, data_type_t,
27
+ gclass_create, gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
28
+ gobj_send_event, register_c_timer, set_log_callback,
29
+ } = await import("@yuneta/gobj-js");
30
+ const {register_c_g6_nodes_tree} = await import("./c_g6_nodes_tree.js");
31
+
32
+ const logged = [];
33
+ const published = [];
34
+
35
+ /* G6, as far as these paths ask it: plugins by key, and nothing on
36
+ * screen. Any other method answers undefined. */
37
+ function fake_graph(history)
38
+ {
39
+ let plugins = [];
40
+ const known = {
41
+ rendered: true,
42
+ getPlugins: () => plugins,
43
+ setPlugins: (fn) => {
44
+ plugins = (typeof fn === "function") ? fn(plugins) : fn;
45
+ },
46
+ getPluginInstance: (key) => (key === "history") ? history : {on() {}, off() {}},
47
+ updatePlugin: () => {},
48
+ getBehaviors: () => [],
49
+ setBehaviors: () => {},
50
+ getData: () => ({nodes: [], edges: [], combos: []}),
51
+ getZoom: () => 1,
52
+ };
53
+ return new Proxy(known, {
54
+ get: (t, k) => (k in t) ? t[k] : (() => undefined)
55
+ });
56
+ }
57
+
58
+ let yuno = null;
59
+
60
+ beforeAll(() => {
61
+ gobj_start_up(null, null, null, null, null, null, null);
62
+ set_log_callback((level, msg) => {
63
+ logged.push({level: String(level), msg: String(msg)});
64
+ });
65
+ register_c_timer();
66
+ register_c_g6_nodes_tree();
67
+ /* The host: what the tree publishes lands here. */
68
+ const host_events = ["EV_UPDATE_NODE"];
69
+ gclass_create(
70
+ "C_TEST_GRAPH_HOST",
71
+ host_events.map((e) => [e, 0]),
72
+ [["ST_IDLE", host_events.map((e) => [e, (gobj, ev, kw) => {
73
+ published.push({event: ev, kw: kw});
74
+ return 0;
75
+ }, null])]],
76
+ {}, 0,
77
+ [SDATA(data_type_t.DTP_STRING, "node_uuid", 0, "test-node-uuid", ""), SDATA_END()],
78
+ {}, 0, 0, 0, 0
79
+ );
80
+ yuno = gobj_create_yuno("owed_yuno", "C_TEST_GRAPH_HOST", {});
81
+ });
82
+
83
+ beforeEach(() => {
84
+ logged.length = 0;
85
+ published.length = 0;
86
+ });
87
+
88
+ let seq = 0;
89
+
90
+ /* A tree whose `users` arrangement the backend holds, in `mode`,
91
+ * with a Save button in its container and a history that has
92
+ * nothing to undo. */
93
+ function build(mode)
94
+ {
95
+ const $container = document.createElement("div");
96
+ const tree = gobj_create_pure_child(`tree_${++seq}`, "C_G6_NODES_TREE",
97
+ {$container: $container}, yuno);
98
+ const listeners = [];
99
+ const history = {
100
+ undo: false,
101
+ canUndo() {
102
+ return this.undo;
103
+ },
104
+ canRedo: () => false,
105
+ on: (ev, fn) => {
106
+ listeners.push(fn);
107
+ },
108
+ clear() {
109
+ this.undo = false;
110
+ }
111
+ };
112
+ tree.priv.graph = fake_graph(history);
113
+ tree.priv.graph_rendered = true;
114
+ tree.priv.treedb_name = "treedb_x";
115
+ tree.priv._graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
116
+ tree.priv._saved_graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
117
+
118
+ const $save = document.createElement("div");
119
+ $save.className = "EV_SAVE_GRAPH";
120
+ $save.setAttribute("disabled", "");
121
+ $container.appendChild($save);
122
+
123
+ gobj_send_event(tree, "EV_SET_OPERATION_MODE", {operation_mode: mode}, tree);
124
+ return {
125
+ tree,
126
+ lit: () => !$save.hasAttribute("disabled"),
127
+ history_changed: () => {
128
+ for(const fn of listeners) {
129
+ fn();
130
+ }
131
+ }
132
+ };
133
+ }
134
+
135
+ function errors()
136
+ {
137
+ return logged.filter((l) => l.level === "error").map((l) => l.msg);
138
+ }
139
+
140
+ describe("a refused __graphs__ write keeps the Save lit", () => {
141
+
142
+ test("in edition: a history change does not put it out", () => {
143
+ const g = build("edition");
144
+ expect(g.lit()).toBe(false);
145
+
146
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
147
+ expect(g.lit()).toBe(true);
148
+
149
+ g.history_changed(); /* nothing to undo */
150
+ expect(g.lit()).toBe(true);
151
+
152
+ gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
153
+ expect(g.lit()).toBe(true);
154
+ expect(errors()).toEqual([]);
155
+ });
156
+
157
+ test("heard in reading, it is shown on entering edition", () => {
158
+ const g = build("reading");
159
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
160
+
161
+ gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
162
+ expect(g.lit()).toBe(true);
163
+ expect(errors()).toEqual([]);
164
+ });
165
+
166
+ test("the Save writes it again, and then the Save goes dark", () => {
167
+ const g = build("edition");
168
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
169
+
170
+ gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
171
+ const writes = published.filter((p) => p.event === "EV_UPDATE_NODE");
172
+ expect(writes.map((w) => w.kw.record.id)).toEqual(["users"]);
173
+ expect(g.lit()).toBe(false);
174
+
175
+ g.history_changed();
176
+ expect(g.lit()).toBe(false);
177
+ expect(errors()).toEqual([]);
178
+ });
179
+
180
+ test("refused again after that Save: lit again", () => {
181
+ const g = build("edition");
182
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
183
+ gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
184
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
185
+
186
+ g.history_changed();
187
+ expect(g.lit()).toBe(true);
188
+ expect(errors()).toEqual([]);
189
+ });
190
+
191
+ test("a reload forgets it: the view holds what the backend holds", () => {
192
+ const g = build("edition");
193
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
194
+ gobj_send_event(g.tree, "EV_CLEAR_DATA", {}, g.tree);
195
+ /* What the load does once the data is in again. */
196
+ gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
197
+ expect(g.lit()).toBe(false);
198
+ expect(errors()).toEqual([]);
199
+ });
200
+ });
@@ -15,7 +15,7 @@ const model = {
15
15
 
16
16
  describe("mark_host_drafts", () => {
17
17
 
18
- it("marks the topics the host names, by treedb (N13)", () => {
18
+ it("marks the topics the host names, by treedb", () => {
19
19
  /* The mark lived in the memory of one session: after a reload the
20
20
  * editor showed no draft while __system__ still differed from the
21
21
  * file in use. Rebuilt from the host's data. */
@@ -68,7 +68,7 @@ describe("host_draft_ids", () => {
68
68
  .toEqual({"treedb_a^users": true, "treedb_b^users": true});
69
69
  });
70
70
 
71
- it("is a fresh map every time: what the host stopped naming is gone (M1)", () => {
71
+ it("is a fresh map every time: what the host stopped naming is gone", () => {
72
72
  /* mark_host_drafts() only ADDS, and the editor fed it the same map
73
73
  * every time: a topic the host named before the Save stayed a
74
74
  * draft after it. */
@@ -138,7 +138,7 @@ describe("EV_DRAFTS: the host is the truth", () => {
138
138
  expect(drafts_on_screen(editor)).toBe(1);
139
139
  });
140
140
 
141
- test("the host names none any more (a Save): the mark goes (M1)", () => {
141
+ test("the host names none any more (a Save): the mark goes", () => {
142
142
  /* It only ever ADDED: after the Save the editor still said
143
143
  * "unsaved schema changes". */
144
144
  const {editor, host} = build("d2");
@@ -316,6 +316,8 @@ const WAIT = "the schemas are loading: wait for them";
316
316
  const STALE = "the schemas were read again: open the dialog again";
317
317
  const KEPT = "cannot read the schemas again: the ones shown may be out of date, your next change reads them first";
318
318
  const OWED = "the schemas shown may be out of date: they are read again, try again when they are in";
319
+ const OWED_FORM = "the schemas shown may be out of date: they are read again, and the form opens again on them with your changes";
320
+ const GONE = "the schemas were read again and what the form was editing is not there any more";
319
321
  const NOT_SENT = "the treedb did not describe a write back: the writes after it were not sent";
320
322
 
321
323
  describe("the screen of a reload", () => {
@@ -618,6 +620,7 @@ describe("a load that fails IN session", () => {
618
620
  const {editor, remote, host} = build("b1", "db/users");
619
621
  gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
620
622
  const form = modals[modals.length - 1];
623
+ form.$content.querySelector('[data-name="header"]').value = "Identifier";
621
624
  gobj_send_event(editor, "EV_REFRESH", {}, host);
622
625
  const asked = take("nodes");
623
626
  answer(editor, remote, asked[0], -1, null, "deadline");
@@ -634,16 +637,28 @@ describe("a load that fails IN session", () => {
634
637
 
635
638
  /* The form was built on a model that may be older than the
636
639
  * store: its Save would write every field of it. It runs the
637
- * reload owed instead, and the load that lands
638
- * closes the form. */
640
+ * reload owed instead. The load that lands closes the form,
641
+ * and opens it again ON THE SCHEMAS JUST READ with what the
642
+ * operator changed: closed alone, what was typed was lost. */
639
643
  form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
640
644
  expect(not_defined()).toEqual([]);
641
645
  expect(take("update-node")).toEqual([]);
642
- expect(shown).toEqual([KEPT, OWED]);
646
+ expect(shown).toEqual([KEPT, OWED_FORM]);
647
+ const before = modals.length;
643
648
  expect(answer_the_load(editor, remote)).toBe(3);
644
649
  expect(form.closed).toBe(true);
645
- expect(shown).toEqual([KEPT, OWED, STALE]);
650
+ expect(modals.length).toBe(before + 1);
651
+ const again = modals[modals.length - 1];
652
+ expect(again.closed).toBe(false);
653
+ expect(again.$content.querySelector('[data-name="header"]').value).toBe("Identifier");
654
+ expect(shown).toEqual([KEPT, OWED_FORM]);
646
655
  expect(editor.priv.reload_on_open).toBe(false);
656
+
657
+ /* Its Save writes, on the record just read. */
658
+ again.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
659
+ const [write] = take("update-node");
660
+ expect(write.kw.record.id).toBe("db.users.id");
661
+ expect(write.kw.record.header).toBe("Identifier");
647
662
  expect(errors()).toEqual([]);
648
663
  });
649
664
 
@@ -1353,3 +1368,297 @@ describe("a move sent by the host while a dialog is up", () => {
1353
1368
  expect(shown).toEqual([]);
1354
1369
  });
1355
1370
  });
1371
+
1372
+ describe("the form opened again after the owed reload", () => {
1373
+
1374
+ function owing(name, subpath)
1375
+ {
1376
+ const built = build(name, subpath);
1377
+ built.editor.priv.reload_on_open = true; /* as a load refused in session leaves it */
1378
+ return built;
1379
+ }
1380
+
1381
+ test("only what was CHANGED is carried: a field the store changed meanwhile shows the store's", () => {
1382
+ const {editor, remote, host} = build("ro6", "db/users");
1383
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1384
+ editor.priv.reload_on_open = true;
1385
+ const form = modals[modals.length - 1];
1386
+ form.$content.querySelector('[data-name="header"]').value = "Identifier";
1387
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1388
+ answer_the_load_with(editor, remote, records_with((r) => {
1389
+ r.cols[0].description = "written by somebody else";
1390
+ }));
1391
+ const again = modals[modals.length - 1];
1392
+ expect(again.$content.querySelector('[data-name="header"]').value).toBe("Identifier");
1393
+ expect(again.$content.querySelector('[data-name="description"]').value)
1394
+ .toBe("written by somebody else");
1395
+ again.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1396
+ const [write] = take("update-node");
1397
+ expect(write.kw.record.description).toBe("written by somebody else");
1398
+ expect(write.kw.record.header).toBe("Identifier");
1399
+ expect(errors()).toEqual([]);
1400
+ });
1401
+
1402
+ test("a NEW column comes back whole: its name, its type and its flags", () => {
1403
+ const {editor, remote, host} = owing("ro2", "db/users");
1404
+ /* The owed reload runs on the form's opening too: open it on a
1405
+ * model that is current, then owe the reload. */
1406
+ editor.priv.reload_on_open = false;
1407
+ gobj_send_event(editor, "EV_ADD_COLUMN", {}, host);
1408
+ editor.priv.reload_on_open = true;
1409
+ const form = modals[modals.length - 1];
1410
+ form.$content.querySelector('[data-name="value"]').value = "email";
1411
+ form.$content.querySelector('[data-name="header"]').value = "Email";
1412
+ for(const $box of form.$content.querySelectorAll(".SCHEMA_FLAG_BOX")) {
1413
+ if($box.dataset.flag === "required") {
1414
+ $box.checked = true;
1415
+ }
1416
+ }
1417
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1418
+ expect(shown).toEqual([OWED_FORM]);
1419
+ answer_the_load(editor, remote);
1420
+ const again = modals[modals.length - 1];
1421
+ expect(again).not.toBe(form);
1422
+ expect(again.$content.querySelector('[data-name="value"]').value).toBe("email");
1423
+ expect(again.$content.querySelector('[data-name="header"]').value).toBe("Email");
1424
+ const on = [...again.$content.querySelectorAll(".SCHEMA_FLAG_BOX")]
1425
+ .filter(($b) => $b.checked).map(($b) => $b.dataset.flag);
1426
+ expect(on).toContain("required");
1427
+ expect(errors()).toEqual([]);
1428
+ });
1429
+
1430
+ test("a topic form comes back the same way", () => {
1431
+ const {editor, remote, host} = build("ro3", "db");
1432
+ gobj_send_event(editor, "EV_EDIT_TOPIC", {topic: "users"}, host);
1433
+ editor.priv.reload_on_open = true;
1434
+ const form = modals[modals.length - 1];
1435
+ form.$content.querySelector('[data-name="tkey"]').value = "tm";
1436
+ form.$content.querySelector(".SCHEMA_TOPIC_FORM_SAVE").click();
1437
+ expect(shown).toEqual([OWED_FORM]);
1438
+ answer_the_load(editor, remote);
1439
+ const again = modals[modals.length - 1];
1440
+ expect(again).not.toBe(form);
1441
+ expect(again.$content.querySelector('[data-name="tkey"]').value).toBe("tm");
1442
+ expect(errors()).toEqual([]);
1443
+ });
1444
+
1445
+ test("what the form edited is gone after the reload: not opened again, and SAID", () => {
1446
+ const {editor, remote, host} = build("ro4", "db/users");
1447
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1448
+ editor.priv.reload_on_open = true;
1449
+ const form = modals[modals.length - 1];
1450
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1451
+ const before = modals.length;
1452
+ answer_the_load_with(editor, remote, records_with((r) => {
1453
+ r.cols = [];
1454
+ }));
1455
+ expect(form.closed).toBe(true);
1456
+ expect(modals.length).toBe(before);
1457
+ expect(shown).toEqual([OWED_FORM, GONE]);
1458
+ expect(errors()).toEqual([]);
1459
+ });
1460
+
1461
+ test("a form closed by the operator while the reload ran is not opened again", () => {
1462
+ const {editor, remote, host} = build("ro5", "db/users");
1463
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1464
+ editor.priv.reload_on_open = true;
1465
+ const form = modals[modals.length - 1];
1466
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1467
+ form.$content.querySelector(".SCHEMA_COL_FORM_CANCEL").click();
1468
+ const before = modals.length;
1469
+ answer_the_load(editor, remote);
1470
+ expect(modals.length).toBe(before);
1471
+ expect(shown).toEqual([OWED_FORM]);
1472
+ expect(errors()).toEqual([]);
1473
+ });
1474
+ });
1475
+
1476
+ describe("EV_REFRESH while a write is in flight", () => {
1477
+
1478
+ test("waits for the end of the writes: the queue is written whole, then the schemas are read", () => {
1479
+ const {editor, remote, host} = build("rq1", "db");
1480
+ gobj_send_event(editor, "EV_CONFIRMED", {what: "topic", topic: "users",
1481
+ model_gen: editor.priv.model_gen}, editor);
1482
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
1483
+ const [first] = take("delete-node");
1484
+ expect(first.kw.topic_name).toBe("cols");
1485
+
1486
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1487
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
1488
+ expect(take("nodes")).toEqual([]);
1489
+ expect(logged.some((l) => l.level === "warning" &&
1490
+ /EV_REFRESH.*when the writes end/.test(l.msg))).toBe(true);
1491
+
1492
+ answer(editor, remote, first, 0, {});
1493
+ const [second] = take("delete-node");
1494
+ expect(second.kw.topic_name).toBe("topics");
1495
+ answer(editor, remote, second, 0, {});
1496
+
1497
+ expect(gobj_current_state(editor)).toBe("ST_LOADING");
1498
+ expect(answer_the_load(editor, remote)).toBe(3);
1499
+ expect(editor.priv.written).toEqual({});
1500
+ expect(errors()).toEqual([]);
1501
+ expect(not_defined()).toEqual([]);
1502
+ });
1503
+ });
1504
+
1505
+ describe("the body while a write is in flight", () => {
1506
+
1507
+ /* pointer-events stops the mouse and nothing else: the shell's
1508
+ * focus trap puts the focus back on the row control that had it,
1509
+ * and Delete or Enter there sent an action ST_SAVING does not
1510
+ * declare. `inert` takes the body out of the keyboard's reach too. */
1511
+ test("is inert, and answers again when the write ends", () => {
1512
+ const {editor, remote, host} = build("r16k1", "db/users");
1513
+ const $body = $in(editor, ".SCHEMA_BODY");
1514
+ expect($body.hasAttribute("inert")).toBe(false);
1515
+
1516
+ const write = start_a_write(editor, host, "name");
1517
+ expect($body.hasAttribute("inert")).toBe(true);
1518
+
1519
+ answer(editor, remote, write, 0, write.kw.record);
1520
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1521
+ expect($in(editor, ".SCHEMA_BODY").hasAttribute("inert")).toBe(false);
1522
+ expect(errors()).toEqual([]);
1523
+ });
1524
+
1525
+ test("a drop that ends the write gives the body back too", () => {
1526
+ const {editor, remote, host} = build("r16k2", "db/users");
1527
+ start_a_write(editor, host, "name");
1528
+ drop(editor, remote, host);
1529
+ expect($in(editor, ".SCHEMA_BODY").hasAttribute("inert")).toBe(false);
1530
+ });
1531
+ });
1532
+
1533
+ describe("the writes of an import mark the topic they write", () => {
1534
+
1535
+ /* An import that adds a topic, `roles`. */
1536
+ const IMPORT = JSON.stringify({id: "db", topics: [
1537
+ {id: "users", pkey: "id", cols: [
1538
+ {id: "id", type: "string", flag: ["persistent", "required"]}
1539
+ ]},
1540
+ {id: "roles", pkey: "id", cols: [
1541
+ {id: "id", type: "string", flag: ["persistent", "required"]}
1542
+ ]}
1543
+ ]});
1544
+
1545
+ /* The record as the store answers it: with the id it gave a new
1546
+ * one, and its fkeys in list_dict. */
1547
+ function as_the_store_answers(write)
1548
+ {
1549
+ const record = JSON.parse(JSON.stringify(write.kw.record));
1550
+ const to_dict = (refs) => refs.map((ref) => {
1551
+ const [topic_name, id, hook_name] = ref.split("^");
1552
+ return {topic_name, id, hook_name};
1553
+ });
1554
+ if(write.kw.topic_name === "topics") {
1555
+ record.id = record.id || `db.${record.value}`;
1556
+ record.treedbs = to_dict(record.treedbs || []);
1557
+ }
1558
+ if(write.kw.topic_name === "cols") {
1559
+ record.id = record.id || `${record.topics[0].split("^")[1]}.${record.value}`;
1560
+ record.topics = to_dict(record.topics || []);
1561
+ }
1562
+ return record;
1563
+ }
1564
+
1565
+ function run_the_import(editor, host)
1566
+ {
1567
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
1568
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
1569
+ model_gen: editor.priv.model_gen}, editor);
1570
+ expect(editor.priv.import_plan.writes.length).toBeGreaterThan(0);
1571
+ gobj_send_event(editor, "EV_APPLY_IMPORT", {model_gen: editor.priv.model_gen}, editor);
1572
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
1573
+ }
1574
+
1575
+ function answer_every_write(editor, remote)
1576
+ {
1577
+ for(let i = 0; i < 10; i++) {
1578
+ const [write] = take("update-node");
1579
+ if(!write) {
1580
+ break;
1581
+ }
1582
+ answer(editor, remote, write, 0, as_the_store_answers(write));
1583
+ }
1584
+ }
1585
+
1586
+ test("from another topic's screen: the topic it wrote, not the one on screen", () => {
1587
+ const {editor, remote, host} = build("r16m1", "db/users");
1588
+ run_the_import(editor, host);
1589
+ answer_every_write(editor, remote);
1590
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1591
+ expect(editor.priv.written).toEqual({"db.roles": true});
1592
+ expect(errors()).toEqual([]);
1593
+ });
1594
+
1595
+ test("from the topics screen, where no topic is on screen", () => {
1596
+ const {editor, remote, host} = build("r16m2", "db");
1597
+ run_the_import(editor, host);
1598
+ answer_every_write(editor, remote);
1599
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1600
+ expect(editor.priv.written).toEqual({"db.roles": true});
1601
+ expect(errors()).toEqual([]);
1602
+ });
1603
+
1604
+ test("a column answered with no record: the record it queued says the topic", () => {
1605
+ const {editor, remote, host} = build("r16m3", "db/users");
1606
+ run_the_import(editor, host);
1607
+ const [topic] = take("update-node");
1608
+ expect(topic.kw.topic_name).toBe("topics");
1609
+ answer(editor, remote, topic, 0, as_the_store_answers(topic));
1610
+ const [col] = take("update-node");
1611
+ expect(col.kw.topic_name).toBe("cols");
1612
+ answer(editor, remote, col, 0, null);
1613
+ expect(editor.priv.written).toEqual({"db.roles": true});
1614
+ });
1615
+ });
1616
+
1617
+ describe("the reason a load failed", () => {
1618
+
1619
+ test("is a key the notice translates, not a string frozen in one language", () => {
1620
+ const remote = gobj_create_service("r16e1_remote", "C_TEST_REMOTE", {}, yuno);
1621
+ gobj_change_state(remote, "ST_SESSION");
1622
+ const host = gobj_create("r16e1_host", "C_TEST_EDITOR_HOST", {}, yuno);
1623
+ const editor = gobj_create("r16e1_editor", "C_YUI_SCHEMA_EDITOR", {
1624
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1625
+ }, host);
1626
+ gobj_start(editor);
1627
+ for(const c of take("nodes")) {
1628
+ answer(editor, remote, c, -1, null, "");
1629
+ }
1630
+ expect(gobj_current_state(editor)).toBe("ST_IDLE");
1631
+ expect(editor.priv.load_error).toBe("cannot load the schemas");
1632
+ /* The notice already says it: no detail repeating the title. */
1633
+ expect($in(editor, ".SCHEMA_NOTICE_DETAIL")).toBe(null);
1634
+ });
1635
+
1636
+ test("the backend's own words travel as its key", () => {
1637
+ const remote = gobj_create_service("r16e2_remote", "C_TEST_REMOTE", {}, yuno);
1638
+ gobj_change_state(remote, "ST_SESSION");
1639
+ const host = gobj_create("r16e2_host", "C_TEST_EDITOR_HOST", {}, yuno);
1640
+ const editor = gobj_create("r16e2_editor", "C_YUI_SCHEMA_EDITOR", {
1641
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1642
+ }, host);
1643
+ gobj_start(editor);
1644
+ for(const c of take("nodes")) {
1645
+ answer(editor, remote, c, -1, null, "treedb not found");
1646
+ }
1647
+ const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
1648
+ expect($detail.getAttribute("data-i18n")).toBe("treedb not found");
1649
+ });
1650
+
1651
+ test("a load that cannot leave keeps its key", () => {
1652
+ const remote = gobj_create_service("r16e3_remote", "C_TEST_REMOTE", {}, yuno);
1653
+ const host = gobj_create("r16e3_host", "C_TEST_EDITOR_HOST", {}, yuno);
1654
+ const editor = gobj_create("r16e3_editor", "C_YUI_SCHEMA_EDITOR", {
1655
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1656
+ }, host);
1657
+ gobj_start(editor);
1658
+ gobj_change_state(remote, "ST_DISCONNECTED");
1659
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1660
+ expect(editor.priv.load_error).toBe("cannot reach the treedb");
1661
+ const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
1662
+ expect($detail.getAttribute("data-i18n")).toBe("cannot reach the treedb");
1663
+ });
1664
+ });