@yuneta/gobj-ui 7.25.13 → 7.25.15

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.
@@ -1,37 +1,35 @@
1
1
  /***********************************************************************
2
2
  * schema_editor_reload.wiring.test.js
3
3
  *
4
- * C_YUI_SCHEMA_EDITOR while it RELOADS (the third independent
5
- * review, 2026-09-23). A reload entered ST_LOADING and drew
6
- * nothing: the screen it replaced stayed up and clickable, and
7
- * ST_LOADING declares none of its actions, so a click on a card,
8
- * on Back, on the drawing -- or the Save of a column form left
9
- * open -- answered "Event NOT DEFINED in state ST_LOADING" and
4
+ * C_YUI_SCHEMA_EDITOR while it RELOADS. A reload entered
5
+ * ST_LOADING and drew nothing: the screen it replaced stayed up and
6
+ * clickable, and ST_LOADING declares none of its actions, so a click
7
+ * on a card, on Back, on the drawing -- or the Save of a column form
8
+ * left open -- answered "Event NOT DEFINED in state ST_LOADING" and
10
9
  * the click, or the edit, was lost.
11
10
  *
12
- * And the lows of the same review: a position left over from a
13
- * load that could not leave, a drop during a WRITE that dropped
14
- * the host's position (a drop during a load applies it), the
15
- * drawing of a treedb that is gone drawn over the old screen, and
16
- * the reload of a late write that forgot this session's marks and
17
- * never told the host a write had landed.
11
+ * And what a reload left behind: a position left over from a load
12
+ * that could not leave, a drop during a WRITE that dropped the
13
+ * host's position (a drop during a load applies it), the drawing of
14
+ * a treedb that is gone drawn over the old screen, the reload of a
15
+ * late write that forgot this session's marks and never told the
16
+ * host a write had landed, a dialog opened on the model it replaced
17
+ * (its Save wrote the old record over the newer one), a load refused
18
+ * IN session that blanked the model, a late write whose list_dict
19
+ * fkey marked nothing, a write answered with no record that owed a
20
+ * second reload and dropped the rest of its queue in silence, and a
21
+ * toolbar gated on the state instead of on the treedb being there.
18
22
  *
19
- * The fourth independent review (same day) found what a reload
20
- * still left behind: a dialog opened on the model it replaced
21
- * (its Save wrote the old record over the newer one), a load
22
- * refused IN session that blanked the model, a late write whose
23
- * list_dict fkey marked nothing, a write answered with no record
24
- * that owed a second reload and dropped the rest of its queue in
25
- * silence, and a toolbar gated on the state instead of on the
26
- * treedb being there.
23
+ * And where the owed reload lingered: a load that LANDED by another
24
+ * road (the operator's Refresh, a late write) left it owed, so the
25
+ * next edit was refused and read the store again; a move sent by the
26
+ * host did not run it; a load that failed forgot the import plan of
27
+ * a dialog it left up; and the export's two views switched in a DOM
28
+ * handler, out of the machine.
27
29
  *
28
- * The sixth independent review (same day) found where the owed
29
- * reload still lingered: a load that LANDED by another road (the
30
- * operator's Refresh, a late write) left it owed, so the next
31
- * edit was refused and read the store again; a move sent by the
32
- * host did not run it; a load that failed forgot the import plan
33
- * of a dialog it left up; and the export's two views switched in
34
- * a DOM handler, out of the machine.
30
+ * And a dialog that outlives its screen through a move sent by the
31
+ * host: its Save answered "Event NOT DEFINED", and a confirmation
32
+ * answered after a move to another treedb deleted there.
35
33
  *
36
34
  * Driven through the FSM on a document double, with a fake
37
35
  * transport whose state is the one the library reads.
@@ -63,8 +61,10 @@ vi.mock("./shell_modals.js", () => ({
63
61
  }};
64
62
  },
65
63
  yui_shell_confirm_danger: (shell, message, opts) => {
66
- confirms.push({message, opts});
67
- return new Promise(() => {});
64
+ /* Answered by the test that needs it (`resolve`), or never. */
65
+ return new Promise((resolve) => {
66
+ confirms.push({message, opts, resolve});
67
+ });
68
68
  },
69
69
  }));
70
70
 
@@ -530,7 +530,7 @@ describe("a write given up that was DONE", () => {
530
530
  });
531
531
  });
532
532
 
533
- describe("a dialog built on the model a reload replaced (fourth review)", () => {
533
+ describe("a dialog built on the model a reload replaced", () => {
534
534
 
535
535
  test("an edit form: the load that lands closes it, and its Save writes nothing", () => {
536
536
  const {editor, remote, host} = build("s1", "db/users");
@@ -588,7 +588,7 @@ describe("a dialog built on the model a reload replaced (fourth review)", () =>
588
588
  r.cols.push({id: "db.users.email", value: "email", order: 2, type: "integer",
589
589
  topics: ["topics^db.users^cols"]});
590
590
  }));
591
- /* Forgotten when the load LANDS, not when it leaves (sixth review). */
591
+ /* Forgotten when the load LANDS, not when it leaves. */
592
592
  expect(editor.priv.import_plan).toBe(null);
593
593
  expect(dialog.closed).toBe(true);
594
594
 
@@ -612,7 +612,7 @@ describe("a dialog built on the model a reload replaced (fourth review)", () =>
612
612
  });
613
613
  });
614
614
 
615
- describe("a load that fails IN session (fourth review)", () => {
615
+ describe("a load that fails IN session", () => {
616
616
 
617
617
  test("keeps the model, the screen and the open form, says so, and owes the reload", () => {
618
618
  const {editor, remote, host} = build("b1", "db/users");
@@ -634,7 +634,7 @@ describe("a load that fails IN session (fourth review)", () => {
634
634
 
635
635
  /* The form was built on a model that may be older than the
636
636
  * store: its Save would write every field of it. It runs the
637
- * reload owed instead (fifth review), and the load that lands
637
+ * reload owed instead, and the load that lands
638
638
  * closes the form. */
639
639
  form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
640
640
  expect(not_defined()).toEqual([]);
@@ -675,7 +675,7 @@ describe("a load that fails IN session (fourth review)", () => {
675
675
  });
676
676
  });
677
677
 
678
- describe("a late write, in the shape the store answers (fourth review)", () => {
678
+ describe("a late write, in the shape the store answers", () => {
679
679
 
680
680
  test("a list_dict fkey marks the topic the column belongs to", () => {
681
681
  const {editor, remote, host} = build("c1", "db/users");
@@ -692,7 +692,7 @@ describe("a late write, in the shape the store answers (fourth review)", () => {
692
692
  });
693
693
  });
694
694
 
695
- describe("a write answered with no record (fourth review)", () => {
695
+ describe("a write answered with no record", () => {
696
696
 
697
697
  test("the reload it asks is the one owed: the next write does not load again", () => {
698
698
  const {editor, remote, host} = build("n1", "db/users");
@@ -736,7 +736,7 @@ describe("a write answered with no record (fourth review)", () => {
736
736
  });
737
737
  });
738
738
 
739
- describe("the toolbar of a treedb that is not there (fourth review)", () => {
739
+ describe("the toolbar of a treedb that is not there", () => {
740
740
 
741
741
  test("offers Back and Refresh, and nothing that needs the treedb", () => {
742
742
  const {editor, remote, host} = build("t1", "db");
@@ -773,7 +773,7 @@ describe("the toolbar of a treedb that is not there (fourth review)", () => {
773
773
  });
774
774
  });
775
775
 
776
- describe("every control of the editor's dialogs is named (fourth review)", () => {
776
+ describe("every control of the editor's dialogs is named", () => {
777
777
 
778
778
  /* A control a wrapping <label> names is the one shape that needs no
779
779
  * attribute (CLAUDE.md, "title + aria-label"). */
@@ -810,7 +810,7 @@ describe("every control of the editor's dialogs is named (fourth review)", () =>
810
810
  });
811
811
  });
812
812
 
813
- describe("the export's two views are controls (fifth review)", () => {
813
+ describe("the export's two views are controls", () => {
814
814
 
815
815
  test("each is a named button that says which one is shown, and switches the text", () => {
816
816
  const {editor, remote, host} = build("x1", "db");
@@ -840,7 +840,7 @@ describe("the export's two views are controls (fifth review)", () => {
840
840
  });
841
841
  });
842
842
 
843
- describe("a Refresh asked while a load is in flight (fifth review)", () => {
843
+ describe("a Refresh asked while a load is in flight", () => {
844
844
 
845
845
  test("keeps the records of the model shown, not the half the first load got", () => {
846
846
  const {editor, remote, host} = build("h1", "db/users");
@@ -876,7 +876,7 @@ describe("a Refresh asked while a load is in flight (fifth review)", () => {
876
876
  });
877
877
  });
878
878
 
879
- describe("the reload a load refused in session owes (fifth review)", () => {
879
+ describe("the reload a load refused in session owes", () => {
880
880
 
881
881
  function refused_load(name, subpath)
882
882
  {
@@ -956,7 +956,7 @@ describe("the reload a load refused in session owes (fifth review)", () => {
956
956
  });
957
957
  });
958
958
 
959
- describe("the import plan (fifth review)", () => {
959
+ describe("the import plan", () => {
960
960
 
961
961
  const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
962
962
  {id: "id", type: "string", flag: ["persistent", "required"]},
@@ -990,7 +990,7 @@ describe("the import plan (fifth review)", () => {
990
990
  });
991
991
  });
992
992
 
993
- describe("a confirmation answered where no treedb is open (fifth review)", () => {
993
+ describe("a confirmation answered where no treedb is open", () => {
994
994
 
995
995
  test("after a reload that landed on zero treedbs: refused as stale, not NOT DEFINED", () => {
996
996
  const {editor, remote, host} = build("e1", "db/users");
@@ -1031,7 +1031,7 @@ describe("a confirmation answered where no treedb is open (fifth review)", () =>
1031
1031
  });
1032
1032
  });
1033
1033
 
1034
- describe("the answers of the editor's confirmations are i18n keys (fifth review, live)", () => {
1034
+ describe("the answers of the editor's confirmations are i18n keys", () => {
1035
1035
 
1036
1036
  /* shell_modals names each answer by its label; its defaults ("Delete",
1037
1037
  * "Cancel") are not lower-case, so no validated locale holds them and
@@ -1046,7 +1046,7 @@ describe("the answers of the editor's confirmations are i18n keys (fifth review,
1046
1046
  });
1047
1047
  });
1048
1048
 
1049
- describe("the owed reload, settled wherever a load lands (sixth review)", () => {
1049
+ describe("the owed reload, settled wherever a load lands", () => {
1050
1050
 
1051
1051
  function refused_load(name, subpath)
1052
1052
  {
@@ -1125,7 +1125,7 @@ describe("the owed reload, settled wherever a load lands (sixth review)", () =>
1125
1125
  });
1126
1126
  });
1127
1127
 
1128
- describe("the import plan outlives a load that fails (sixth review)", () => {
1128
+ describe("the import plan outlives a load that fails", () => {
1129
1129
 
1130
1130
  const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
1131
1131
  {id: "id", type: "string", flag: ["persistent", "required"]},
@@ -1173,7 +1173,7 @@ describe("the import plan outlives a load that fails (sixth review)", () => {
1173
1173
  });
1174
1174
  });
1175
1175
 
1176
- describe("the export's two views go through the machine (sixth review)", () => {
1176
+ describe("the export's two views go through the machine", () => {
1177
1177
 
1178
1178
  test("a click on a view is an event, and the action switches the text", () => {
1179
1179
  const {editor, remote, host} = build("y1", "db");
@@ -1221,3 +1221,135 @@ describe("the export's two views go through the machine (sixth review)", () => {
1221
1221
  expect(errors().length).toBe(2);
1222
1222
  });
1223
1223
  });
1224
+
1225
+ describe("a move sent by the host while a dialog is up", () => {
1226
+
1227
+ /* C_YUI_SHELL keeps the overlays open on a move that changes only
1228
+ * the subpath (a url typed in, a link). The dialog of the screen the
1229
+ * editor left sends what only that screen declares. */
1230
+ const MOVED = "the view moved: open the dialog again";
1231
+
1232
+ const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
1233
+ {id: "id", type: "string", flag: ["persistent", "required"]},
1234
+ {id: "email", type: "string"}
1235
+ ]}]});
1236
+
1237
+ function settle()
1238
+ {
1239
+ return new Promise((resolve) => {
1240
+ setTimeout(resolve, 0);
1241
+ });
1242
+ }
1243
+
1244
+ test("a column form: closed and said, not left to answer NOT DEFINED", () => {
1245
+ const {editor, remote, host} = build("mv1", "db/users");
1246
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1247
+ const form = modals[modals.length - 1];
1248
+ expect(form.closed).toBe(false);
1249
+
1250
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1251
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1252
+ expect(form.closed).toBe(true);
1253
+ expect(editor.priv.dialog).toBe(null);
1254
+ expect(shown).toEqual([MOVED]);
1255
+ expect(not_defined()).toEqual([]);
1256
+ expect(errors()).toEqual([]);
1257
+ expect(commands).toEqual([]);
1258
+ });
1259
+
1260
+ test("the import dialog: closed, and its plan forgotten", () => {
1261
+ const {editor, remote, host} = build("mv2", "db");
1262
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
1263
+ const dialog = modals[modals.length - 1];
1264
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
1265
+ model_gen: editor.priv.model_gen}, editor);
1266
+ expect(editor.priv.import_plan).not.toBe(null);
1267
+
1268
+ gobj_send_event(editor, "EV_SHOW", {subpath: ""}, host);
1269
+ expect(gobj_current_state(editor)).toBe("ST_TREEDBS");
1270
+ expect(dialog.closed).toBe(true);
1271
+ expect(editor.priv.import_plan).toBe(null);
1272
+ expect(shown).toEqual([MOVED]);
1273
+ expect(not_defined()).toEqual([]);
1274
+ expect(errors()).toEqual([]);
1275
+ });
1276
+
1277
+ test("a move that arrives during a load that fails: the same, when it is applied", () => {
1278
+ const {editor, remote, host} = build("mv3", "db/users");
1279
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1280
+ const form = modals[modals.length - 1];
1281
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1282
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1283
+ expect(form.closed).toBe(false);
1284
+ for(const c of take("nodes")) {
1285
+ answer(editor, remote, c, -1, null, "deadline");
1286
+ }
1287
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1288
+ expect(form.closed).toBe(true);
1289
+ expect(shown).toEqual([KEPT, MOVED]);
1290
+ expect(not_defined()).toEqual([]);
1291
+ });
1292
+
1293
+ test("the same position sent again leaves the dialog alone", () => {
1294
+ const {editor, remote, host} = build("mv4", "db/users");
1295
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1296
+ const form = modals[modals.length - 1];
1297
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
1298
+ expect(form.closed).toBe(false);
1299
+ expect(shown).toEqual([]);
1300
+ });
1301
+
1302
+ test("the export and the check read nothing of the screen: they stay up", () => {
1303
+ const {editor, remote, host} = build("mv5", "db");
1304
+ gobj_send_event(editor, "EV_EXPORT", {}, host);
1305
+ const exported = modals[modals.length - 1];
1306
+ gobj_send_event(editor, "EV_SHOW", {subpath: ""}, host);
1307
+ expect(exported.closed).toBe(false);
1308
+ exported.$content.querySelectorAll(".SCHEMA_EXPORT_TAB")[1].click();
1309
+ expect(exported.$content.querySelector(".SCHEMA_EXPORT_TEXT").value.trim()
1310
+ .startsWith("{")).toBe(true);
1311
+
1312
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1313
+ gobj_send_event(editor, "EV_VALIDATE", {}, host);
1314
+ const report = modals[modals.length - 1];
1315
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
1316
+ expect(report.closed).toBe(false);
1317
+ expect(shown).toEqual([]);
1318
+ expect(not_defined()).toEqual([]);
1319
+ expect(errors()).toEqual([]);
1320
+ });
1321
+
1322
+ test("a confirmation answered after a move to another treedb deletes nothing", async () => {
1323
+ const {editor, remote, host} = build("mv6", "db/users");
1324
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1325
+ answer_the_load_with(editor, remote, records_with((r) => {
1326
+ r.treedbs.push({id: "db2", schema_version: 1});
1327
+ r.topics.push({id: "db2.users", value: "users", order: 1, pkey: "id",
1328
+ topic_version: 1, treedbs: ["treedbs^db2^topics"]});
1329
+ r.cols.push({id: "db2.users.id", value: "id", order: 1, type: "string",
1330
+ flag: ["persistent", "required"], topics: ["topics^db2.users^cols"]});
1331
+ }));
1332
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1333
+
1334
+ gobj_send_event(editor, "EV_DELETE_COLUMN", {col: "id"}, host);
1335
+ expect(confirms.length).toBe(1);
1336
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db2/users"}, host);
1337
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1338
+ confirms[0].resolve(true);
1339
+ await settle();
1340
+
1341
+ expect(take("delete-node")).toEqual([]);
1342
+ expect(commands).toEqual([]);
1343
+ expect(shown).toEqual([MOVED]);
1344
+ expect(errors()).toEqual([]);
1345
+ });
1346
+
1347
+ test("a confirmation answered on the screen it was asked from still runs", async () => {
1348
+ const {editor, remote, host} = build("mv7", "db/users");
1349
+ gobj_send_event(editor, "EV_DELETE_COLUMN", {col: "id"}, host);
1350
+ confirms[0].resolve(true);
1351
+ await settle();
1352
+ expect(take("delete-node").length).toBe(1);
1353
+ expect(shown).toEqual([]);
1354
+ });
1355
+ });
@@ -1,8 +1,8 @@
1
1
  /***********************************************************************
2
2
  * schema_editor_transport.wiring.test.js
3
3
  *
4
- * C_YUI_SCHEMA_EDITOR when the transport drops (M-1 of the
5
- * independent review of 7.25.4). A drop used to leave the editor
4
+ * C_YUI_SCHEMA_EDITOR when the transport drops. A drop used to
5
+ * leave the editor
6
6
  * where it was: in ST_SAVING with its body busy, or in ST_LOADING
7
7
  * -- and the reconnect skipped the reload BECAUSE the state was
8
8
  * ST_LOADING. Only a reload of the page got it out.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * "This topic was written in this session and is not saved."
5
5
  *
6
- * An edit is a DRAFT (M36 of yunetas' 2026-09-21 review): it moves
6
+ * An edit is a DRAFT: it moves
7
7
  * no version, and the host's save-schema raises the versions of what
8
8
  * changed. So "written" alone is the whole question -- the version is
9
9
  * no longer measured against a baseline, which is also why the old
@@ -124,9 +124,8 @@ function maybe_apply_translator($node, opts)
124
124
  * it -- until every handle is closed or has timed out. Handed the
125
125
  * first caller's handle and timer, a repeat asking `timeout: 0`
126
126
  * was dismissed on the first one's timer, and closing one
127
- * caller's handle closed a toast another was still showing
128
- * (third independent review). The operator's ✕ closes it for
129
- * everybody.
127
+ * caller's handle closed a toast another was still showing.
128
+ * The operator's ✕ closes it for everybody.
130
129
  ***************************************************************/
131
130
  function show_notification(shell, kind, message, opts)
132
131
  {
@@ -5,17 +5,17 @@
5
5
  * stacked under it: one close of the transport settles every
6
6
  * request it cut, each view that asked shows its failure, and
7
7
  * the operator got a column of identical "the connection
8
- * dropped" (low of the independent review of the 2nd round).
8
+ * dropped".
9
9
  *
10
- * And a repeat is a caller of its own (third independent
11
- * review): its handle and its time are its own, and the toast
12
- * stays while any caller still holds it.
10
+ * And a repeat is a caller of its own: its handle and its time
11
+ * are its own, and the toast stays while any caller still holds
12
+ * it.
13
13
  *
14
14
  * And every ✕ this module draws is a control with a NAME: a
15
- * `title` and an `aria-label`, both translatable (fourth
16
- * independent review: TOAST_CLOSE had the label only). So is
17
- * every other button of a dialog (fifth: the dialog's back arrow
18
- * had no title, and the Yes/No of every confirmation had neither).
15
+ * `title` and an `aria-label`, both translatable (TOAST_CLOSE had
16
+ * the label only). So is every other button of a dialog (the
17
+ * dialog's back arrow had no title, and the Yes/No of every
18
+ * confirmation had neither).
19
19
  *
20
20
  * Copyright (c) 2026, ArtGins.
21
21
  * All Rights Reserved.
@@ -78,7 +78,7 @@ describe("one toast per message on screen", () => {
78
78
  });
79
79
 
80
80
  /*
81
- * The third independent review: a repeat was handed the FIRST toast's
81
+ * A repeat was handed the FIRST toast's
82
82
  * handle and timer. A repeat asking `timeout: 0` was dismissed on the
83
83
  * first one's timer, and closing one caller's handle closed the toast
84
84
  * another caller was still showing.
@@ -168,7 +168,7 @@ describe("every close control is named", () => {
168
168
  });
169
169
  });
170
170
 
171
- describe("every other button of a dialog is named (fifth review)", () => {
171
+ describe("every other button of a dialog is named", () => {
172
172
 
173
173
  function named($b)
174
174
  {
@@ -7,7 +7,7 @@
7
7
  * (only its own `plaintext` formatter escapes), so a record's text
8
8
  * returned as a string is parsed as markup: `a<b and c>d` showed
9
9
  * as `ad`, and a field holding `<img src=x onerror=...>` ran it in
10
- * the operator's browser (M31 of the 2026-09-21 review). Record
10
+ * the operator's browser. Record
11
11
  * data goes in as a TEXT NODE, and a cell built around it as DOM,
12
12
  * never as an HTML string with the data interpolated.
13
13
  *
@@ -0,0 +1,208 @@
1
+ /***********************************************************************
2
+ * treedb_graph_writes.wiring.test.js
3
+ *
4
+ * C_YUI_TREEDB_GRAPH and the writes of `__graphs__`, the topic
5
+ * where the graph keeps its arrangement. The engine
6
+ * (C_G6_NODES_TREE) takes such a write for granted before it
7
+ * leaves: nothing answers it when it lands. So the host must say
8
+ * when one did NOT land -- refused by the backend, refused on the
9
+ * way out, or never sent for want of a session. Without that, the
10
+ * engine believed the backend held the arrangement, and the next
11
+ * Save had nothing to write.
12
+ *
13
+ * Driven through the real host on a document double, with a fake
14
+ * engine that records what it is told and a fake transport whose
15
+ * state is the one the host reads.
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
+ /* The host watches its container's size; the document double has no
26
+ * layout to watch. */
27
+ if(!globalThis.ResizeObserver) {
28
+ globalThis.ResizeObserver = class {
29
+ observe() {}
30
+ unobserve() {}
31
+ disconnect() {}
32
+ };
33
+ }
34
+
35
+ const {
36
+ SDATA, SDATA_END, data_type_t,
37
+ gclass_create,
38
+ gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
39
+ gobj_start, gobj_send_event, gobj_publish_event,
40
+ gobj_read_pointer_attr, gobj_subscribe_event,
41
+ gobj_change_state, gobj_current_state,
42
+ set_log_callback,
43
+ } = await import("@yuneta/gobj-js");
44
+
45
+ const logged = [];
46
+ const commands = []; /* what the fake transport was asked */
47
+ const told = []; /* what the fake engine was told */
48
+ let refuse_on_the_way = false;
49
+
50
+ function remote_command_parser(gobj, command, kw, src)
51
+ {
52
+ if(refuse_on_the_way) {
53
+ return `cannot route '${command}'`;
54
+ }
55
+ commands.push({command, kw, src});
56
+ return null;
57
+ }
58
+
59
+ let yuno = null;
60
+
61
+ beforeAll(async () => {
62
+ gobj_start_up(null, null, null, null, null, null, null);
63
+ set_log_callback((level, msg) => {
64
+ logged.push({level: String(level), msg: String(msg)});
65
+ });
66
+
67
+ gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
68
+ gclass_create(
69
+ "C_TEST_REMOTE",
70
+ [],
71
+ [["ST_SESSION", []], ["ST_DISCONNECTED", []]],
72
+ {mt_command_parser: remote_command_parser},
73
+ 0, [SDATA_END()], {}, 0, 0, 0, 0
74
+ );
75
+
76
+ /* The engine, registered BEFORE the host so the host uses it. It
77
+ * publishes what the real one publishes on a Save, and records every
78
+ * event it is sent. */
79
+ const record = (gobj, event, kw) => {
80
+ told.push({event, kw});
81
+ return 0;
82
+ };
83
+ const ENGINE_EVENTS = [
84
+ "EV_DESCS", "EV_CLEAR_DATA", "EV_LOAD_DATA", "EV_NODE_CREATED",
85
+ "EV_NODE_UPDATED", "EV_NODE_DELETED", "EV_GRAPHS_WRITE_REFUSED",
86
+ "EV_SHOW", "EV_HIDE", "EV_RESIZE", "EV_THEME", "EV_LANGUAGE_CHANGED",
87
+ "EV_SET_OPERATION_MODE", "EV_SET_LAYOUT"
88
+ ];
89
+ gclass_create(
90
+ "C_G6_NODES_TREE",
91
+ ENGINE_EVENTS.map((e) => [e, 0]).concat([["EV_UPDATE_NODE", 2]]),
92
+ [["ST_IDLE", ENGINE_EVENTS.map((e) => [e, record, null])]],
93
+ {
94
+ mt_create: (gobj) => {
95
+ const subscriber = gobj_read_pointer_attr(gobj, "subscriber");
96
+ if(subscriber) {
97
+ gobj_subscribe_event(gobj, null, {}, subscriber);
98
+ }
99
+ }
100
+ },
101
+ 0,
102
+ [
103
+ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, ""),
104
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, ""),
105
+ SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, ""),
106
+ SDATA(data_type_t.DTP_STRING, "treedb_name", 0, "", ""),
107
+ SDATA(data_type_t.DTP_JSON, "layout_names", 0, "[]", ""),
108
+ SDATA(data_type_t.DTP_STRING, "layout", 0, "", ""),
109
+ SDATA_END()
110
+ ],
111
+ {}, 0, 0, 0, 0
112
+ );
113
+
114
+ const {register_c_yui_treedb_graph} = await import("./c_yui_treedb_graph.js");
115
+ register_c_yui_treedb_graph();
116
+
117
+ yuno = gobj_create_yuno("graph_writes_yuno", "C_TEST_HOST", {});
118
+ gobj_start(yuno);
119
+ });
120
+
121
+ beforeEach(() => {
122
+ logged.length = 0;
123
+ commands.length = 0;
124
+ told.length = 0;
125
+ refuse_on_the_way = false;
126
+ });
127
+
128
+ function build(name)
129
+ {
130
+ const remote = gobj_create_service(`${name}_remote`, "C_TEST_REMOTE", {}, yuno);
131
+ gobj_change_state(remote, "ST_SESSION");
132
+ const host = gobj_create_service(`${name}_graph`, "C_YUI_TREEDB_GRAPH", {
133
+ gobj_remote_yuno: remote,
134
+ treedb_name: "treedb_test"
135
+ }, yuno);
136
+ const engine = host.priv.gobj_nodes_tree;
137
+ expect(engine).toBeTruthy();
138
+ return {remote, host, engine};
139
+ }
140
+
141
+ /* What the engine publishes on a Save of one topic's arrangement. */
142
+ function save_arrangement(engine, topic)
143
+ {
144
+ gobj_publish_event(engine, "EV_UPDATE_NODE", {
145
+ treedb_name: "treedb_test",
146
+ topic_name: "__graphs__",
147
+ record: {id: topic, topic: topic, active: true, properties: {nodes: {}}},
148
+ options: {list_dict: true, autolink: false, create: true}
149
+ });
150
+ }
151
+
152
+ function answer(host, remote, request, result, comment)
153
+ {
154
+ gobj_send_event(host, "EV_MT_COMMAND_ANSWER", {
155
+ result: result,
156
+ comment: comment || "",
157
+ data: null,
158
+ __md_iev__: {command_stack: [{command: request.command, kw: request.kw}]}
159
+ }, remote);
160
+ }
161
+
162
+ function refused()
163
+ {
164
+ return told.filter((t) => t.event === "EV_GRAPHS_WRITE_REFUSED").map((t) => t.kw.topic);
165
+ }
166
+
167
+ describe("a __graphs__ write that does not land is said to the engine", () => {
168
+
169
+ test("refused by the backend", () => {
170
+ const {remote, host, engine} = build("g1");
171
+ save_arrangement(engine, "yunos");
172
+ expect(commands.length).toBe(1);
173
+ answer(host, remote, commands[0], -1, "not authorized");
174
+ expect(refused()).toEqual(["yunos"]);
175
+ });
176
+
177
+ test("landed: nothing is said", () => {
178
+ const {remote, host, engine} = build("g2");
179
+ save_arrangement(engine, "yunos");
180
+ answer(host, remote, commands[0], 0);
181
+ expect(refused()).toEqual([]);
182
+ });
183
+
184
+ test("with no session: not sent, and said", () => {
185
+ const {remote, host, engine} = build("g3");
186
+ gobj_change_state(remote, "ST_DISCONNECTED");
187
+ save_arrangement(engine, "realms");
188
+ expect(commands).toEqual([]);
189
+ expect(refused()).toEqual(["realms"]);
190
+ expect(logged.some((l) => /not sent/.test(l.msg))).toBe(true);
191
+ });
192
+
193
+ test("refused on the way out by the transport", () => {
194
+ const {remote, host, engine} = build("g4");
195
+ refuse_on_the_way = true;
196
+ save_arrangement(engine, "binaries");
197
+ expect(refused()).toEqual(["binaries"]);
198
+ });
199
+
200
+ test("a refused write of another topic says nothing to the engine", () => {
201
+ const {remote, host, engine} = build("g5");
202
+ gobj_send_event(host, "EV_UPDATE_NODE", {
203
+ topic_name: "yunos", record: {id: "1", x: 1}
204
+ }, engine);
205
+ answer(host, remote, commands[0], -1, "refused");
206
+ expect(refused()).toEqual([]);
207
+ });
208
+ });