@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.
- package/README.md +61 -15
- package/dist/gobj-ui.cjs.js +209 -74
- package/dist/gobj-ui.es.js +209 -74
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +42 -11
- package/src/c_yui_schema_editor.js +104 -37
- package/src/c_yui_treedb_graph.js +53 -6
- package/src/c_yui_treedb_topic_with_form.js +10 -10
- package/src/c_yui_treedb_topics.js +37 -32
- package/src/form_busy.js +2 -3
- package/src/form_time_value.js +1 -1
- package/src/form_time_value.test.js +1 -1
- package/src/form_writes_in_flight.js +1 -2
- package/src/g6_nodes_tree_fsm.test.js +33 -0
- package/src/graph_save_plan.js +28 -0
- package/src/graph_save_plan.test.js +36 -2
- package/src/host_drafts.js +2 -3
- package/src/schema_editor_drafts.wiring.test.js +1 -1
- package/src/schema_editor_reload.wiring.test.js +178 -46
- package/src/schema_editor_transport.wiring.test.js +2 -2
- package/src/schema_version_bump.test.js +1 -1
- package/src/shell_modals.js +2 -3
- package/src/shell_toast_once.test.js +10 -10
- package/src/table_cell_text.js +1 -1
- package/src/treedb_graph_writes.wiring.test.js +208 -0
- package/src/treedb_topics_transport.wiring.test.js +80 -12
- package/src/yui_row_search.test.js +1 -1
- package/src/yui_table_select.test.js +1 -1
|
@@ -1,37 +1,35 @@
|
|
|
1
1
|
/***********************************************************************
|
|
2
2
|
* schema_editor_reload.wiring.test.js
|
|
3
3
|
*
|
|
4
|
-
* C_YUI_SCHEMA_EDITOR while it RELOADS
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
-
*
|
|
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
|
|
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
|
package/src/shell_modals.js
CHANGED
|
@@ -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
|
-
*
|
|
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"
|
|
8
|
+
* dropped".
|
|
9
9
|
*
|
|
10
|
-
* And a repeat is a caller of its own
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 (
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
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
|
|
171
|
+
describe("every other button of a dialog is named", () => {
|
|
172
172
|
|
|
173
173
|
function named($b)
|
|
174
174
|
{
|
package/src/table_cell_text.js
CHANGED
|
@@ -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
|
|
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
|
+
});
|